Asynchronous ADMM implementation¶
This section explains the implementation of the asynchronous ADMM module. The workflow of the asynchronous ADMM module is established in the following way:
runme_admm.py: runme_admm.py
is the script that has to be run by the user, where the input file for the model, modelled time period and the cluster definition is made.
runfunctions_admm.py: runfunctions_admm.py
is the script that is called by the runme_admm.py
script. Here, the data structures for the subproblems is created, the submodels are built, and asynchronous ADMM processes are launched.
run_Worker.py: ADMM_async/run_Worker.py
includes the function run_worker()
, which is the parallel ADMM routine that are followed asynchronously by the parallel workers. The major argument of this function is a urbsADMMmodel
class, whose methods are defined in the ADMM_async/urbs_admm_model.py
script.
Moreover, minor additions/modifications were done on the following, already existing scripts:
- urbs/input.py
- urbs/model.py
- urbs/features/transmission.py
which will also be mentioned here.
The workflow of the ADMM implementation is illustrated as follows:
In the following, a walkthrough on the scripts involved will be given to establish understanding regarding how the ADMM implementation works.
runme_admm.py¶
Let us start with the imported packages:
import os
import shutil
import urbs
from urbs.runfunctions_admm import *
from multiprocessing import freeze_support
Besides the usual urbs imports os
, shutil
and urbs
, the urbs.runfunctions
module is imported as it contains the urbs.run_regional
function that commences the ADMM routine. Moreover, to allow for parallel operation on Windows systems, the freeze_support
function has to be imported from the multiprocessing
package.
Moving on to the input settings:
The script starts with the specification of the input file, which is to be
located in the same folder as script runme_admm.py
:
# Choose input file
input_files = 'germany.xlsx' # for single year file name, for intertemporal folder name
input_dir = 'Input'
input_path = os.path.join(input_dir, input_files)
Then the result name and the result directory is set:
result_name = 'Run'
result_dir = urbs.prepare_result_directory(result_name) # name + time stamp
Input file is added in the result directory:
# copy input file to result directory
try:
shutil.copytree(input_path, os.path.join(result_dir, input_dir))
except NotADirectoryError:
shutil.copyfile(input_path, os.path.join(result_dir, input_files))
# copy run file to result directory
shutil.copy(__file__, result_dir)
The objective function to be minimized by the model is then determined (options: ‘cost’ or ‘CO2’):
# objective function
objective = 'cost' # set either 'cost' or 'CO2' as objective
Then the specification of time step length and modeled time horizon is made:
# simulation timesteps
(offset, length) = (0, 8760) # time step selection
timesteps = range(offset, offset+length+1)
dt = 1 # length of each time step (unit: hours)
Variable timesteps
is the list of time steps to be modelled. 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 time steps to be
optimised. Finally, the variable dt
gives the width of each timestep, input in hours.
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]
An essential input for the ADMM module is the clustering scheme of the model regions:
clusters = [[('Schleswig-Holstein')],[('Hamburg')],[('Mecklenburg-Vorpommern')],[('Offshore')],[('Lower Saxony')],[('Bremen')],[('Saxony-Anhalt')],[('Brandenburg')],[('Berlin')],[('North Rhine-Westphalia')],
[('Baden-Württemberg')],[('Hesse')],[('Bavaria')],[('Rhineland-Palatinate')],[('Saarland')],[('Saxony')],[('Thuringia')]]
The variable clusters
is a list of tuples lists, where each element consists of tuple lists with the regions to be included in each subproblem. For instance, whereas the clustering given above yields each federal state of the Germany model having their own subproblems, a scheme as following:
clusters = [[('Schleswig-Holstein'),('Hamburg'),('Mecklenburg-Vorpommern'),('Offshore'),('Lower Saxony'),('Bremen'),('Saxony-Anhalt'),('Brandenburg'),('Berlin'),('North Rhine-Westphalia')],
[('Baden-Württemberg'),('Hesse'),('Bavaria'),('Rhineland-Palatinate'),('Saarland'),('Saxony'),('Thuringia')]]
would yield two subproblems, where the northern and southern federal states of Germany are grouped with each other.
Then the color schemes for output plots is defined:
# 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
Scenarios to be run can be then selected:
# select scenarios to be run
test_scenarios = [
urbs.scenario_base
]
Finally, the urbs.run_regional
function is called, commencing the ADMM routine:
if __name__ == '__main__':
freeze_support()
for scenario in test_scenarios:
timesteps = range(offset, offset + length + 1)
prob = urbs.run_regional(input_path, timesteps,
scenario,result_dir,dt,objective,
clusters=clusters)
To read about the urbs.run_regional
function, please proceed to the next section, where the runfunctions_admm.py
script, where this function resides, is described.
runfunctions_admm.py¶
Imports:
from pyomo.environ import SolverFactory
from .model import create_model
from .plot import *
from .input import *
from .validation import *
import urbs
import pandas as pd
import multiprocessing as mp
import queue
from .ADMM_async.run_Worker import run_worker
from .ADMM_async.urbs_admm_model import urbsADMMmodel
import time
import numpy as np
from math import ceil
Besides the usual imports of runfunctions.py
(first group), additional imports are necessary:
multiprocessing
is a package that supports spawning processes using an API similar to the threading module. This is used for creating the objectsmp.Manager().Queue()
andmp.Process()
.queue
is used as an exception handling (queue.Empty
), see later.- The function
run_worker
contains all the ADMM steps that are followed by the submodel classesurbsADMMmodel
. time
is used as a runtime-profiling (for test purposes).numpy
andmath.ceil
are required for array operations and a ceiling function respectively.
Some auxiliary function definitions follow:
def calculate_neighbor_cluster_per_line(boundarying_lines, cluster_idx, clusters):
neighbor_cluster = 99 * np.ones((len(boundarying_lines[cluster_idx]), 2))
row_number = 0
for (year, site_in, site_out, tra, com) in boundarying_lines[cluster_idx].index:
for cluster in clusters:
if site_in in cluster:
neighbor_cluster[row_number, 0] = clusters.index(cluster)
if site_out in cluster:
neighbor_cluster[row_number, 1] = clusters.index(cluster)
row_number = row_number + 1
cluster_from = neighbor_cluster[:, 0]
cluster_to = neighbor_cluster[:, 1]
neighbor_cluster = np.sum(neighbor_cluster, 1) - cluster_idx
return cluster_from, cluster_to, neighbor_cluster
Function calculate_neighbor_cluster_per_line
is applied to each cluster, and returns three arrays:
cluster_from
has a length equal to the boundarying transmission lines of a given cluster, and each value corresponds to the cluster index, that includes the site that is on thesit_in
column of the transmission line,cluster_to
has a length equal to the boundarying transmission lines of a given cluster, and each value corresponds to the cluster index, that includes the site that is on thesit_out
column of the transmission line,neighbor_cluster
has a length equal to the boundarying transmission lines of a given cluster, and each value corresponds to the index of the neighboring cluster that is involved.
def create_queues(clusters, boundarying_lines):
edges = np.empty((1, 2))
for cluster_idx in range(0, len(clusters)):
edges = np.concatenate((edges, np.stack([boundarying_lines[cluster_idx].cluster_from.to_numpy(),
boundarying_lines[cluster_idx].cluster_to.to_numpy()], axis=1)))
edges = np.delete(edges, 0, axis=0)
edges = np.unique(edges, axis=0)
edges = np.array(list({tuple(sorted(item)) for item in edges}))
queues = {}
for edge in edges.tolist():
fend = mp.Manager().Queue()
tend = mp.Manager().Queue()
if edge[0] not in queues:
queues[edge[0]] = {}
queues[edge[0]][edge[1]] = fend
if edge[1] not in queues:
queues[edge[1]] = {}
queues[edge[1]][edge[0]] = tend
return edges, queues
Function create_queues
returns two objects:
edges
is an array with two columns, which expresses the connectivity between the clusters (if clusters are connected in the following way:0--1--2
,edges
would look as follows:[[0, 1], [1, 0], [1, 2], [2, 1]]
),queues
is a dictionary of dictionaries populated withmp.Manager().Queue()
objects. There are as manymp.Manager().Queue()
objects as the rows ofedges
, and these queues are used for the unidirectional data transfer between these clusters during the parallel operation.
Class CouplingVars
is defined to store some coupling parameters:
class CouplingVars:
flow_global = {}
rhos = {}
lambdas = {}
cap_global = {}
residdual = {}
residprim = {}
Functions prepare_result_directory
and setup_solver
are unchanged except enforcing the barrier method for the gurobi solver (method=2
). Please note that only gurobi is supported as a solver in this implementation!:
def prepare_result_directory(result_name):
""" create a time stamped directory within the result folder.
Args:
result_name: user specified result name
Returns:
a subfolder in the result folder
"""
# timestamp for result directory
now = datetime.now().strftime('%Y%m%dT%H%M')
# create result directory if not existent
result_dir = os.path.join('result', '{}-{}'.format(result_name, now))
if not os.path.exists(result_dir):
os.makedirs(result_dir)
return result_dir
def setup_solver(optim, logfile='solver.log'):
""" """
if optim.name == 'gurobi':
# reference with list of option names
# http://www.gurobi.com/documentation/5.6/reference-manual/parameters
optim.set_options("logfile={}".format(logfile))
optim.set_options("method=2")
# optim.set_options("timelimit=7200") # seconds
# optim.set_options("mipgap=5e-4") # default = 1e-4
elif optim.name == 'glpk':
# reference with list of options
# execute 'glpsol --help'
optim.set_options("log={}".format(logfile))
# optim.set_options("tmlim=7200") # seconds
# optim.set_options("mipgap=.0005")
elif optim.name == 'cplex':
optim.set_options("log={}".format(logfile))
else:
print("Warning from setup_solver: no options set for solver "
"'{}'!".format(optim.name))
return optim
Now that the auxiliary functions are explained, the main function of this script, run_regional
, will be explained step by step.
The docstring of the function gives an overview regarding the input and output arguments:
def run_regional(input_file, timesteps, scenario, result_dir,
dt, objective, clusters=None):
""" run an urbs model for given input, time steps and scenario with regional decomposition using ADMM
Args:
input_file: filename to an Excel spreadsheet for urbs.read_excel
timesteps: a list of timesteps, e.g. range(0,8761)
scenario: a scenario function that modifies the input data dict
result_dir: directory name for result spreadsheet and plots
dt: width of a time step in hours(default: 1)
objective: the entity which is optimized ('cost' of 'co2')
clusters: user-defined region clusters for regional decomposition (list of tuple lists)
Returns:
the urbs model instances
"""
First, the model year is hard-coded to be used as the support year (stf
) indices. This is a single scalar, since ADMM, in its current status, does not support intertemporal models:
# hard-coded year. ADMM doesn't work with intertemporal models (yet)
year = date.today().year
Then, similarly to regular urbs, the scenario is set up, the model data is read and and validations are made in the following steps:
# scenario name, read and modify data for scenario
sce = scenario.__name__
data_all = read_input(input_file, year)
data_all = scenario(data_all)
validate_input(data_all)
validate_dc_objective(data_all, objective)
If there is a global CO2 limit set in the model, the necessary modifications to the data structure are made with the add_carbon_supplier
function. These are mentioned in the section Formulation the global CO2 limit in the consensus form. Then, the Carbon site is added as a separate cluster:
if not data_all['global_prop'].loc[year].loc['CO2 limit', 'value'] == np.inf:
data_all = add_carbon_supplier(data_all, clusters)
clusters.append(['Carbon_site'])
A CouplingVars class is initialized:
# initiate a coupling-variables Class
coup_vars = CouplingVars()
In the following code section, the Transmission
DataFrame is sliced for each cluster (with index cluster_idx
), such that boundarying_lines[cluster_idx]
comprises only the transmission lines which are interfacing with a neighboring cluster and, conversely, internal_lines[cluster_idx]
consists of the transmission lines that connect the sites within the cluster. Afterwards, the ADMM parameters coup_vars.lambdas
, coup_vars.rhos
and coup_vars.flow_global
are initialized with the following indices:
cluster_idx
: each cluster index,j
: each modelled time-step,year
: the support timeframe (a single year in this case),sit_from
: first end of the transmission line (obtained fromboundarying_lines[cluster_idx]
)sit_to
: second end of the transmission line (obtained fromboundarying_lines[cluster_idx]
)
# identify the boundarying and internal lines
boundarying_lines = {}
internal_lines = {}
boundarying_lines_logic = np.zeros((len(clusters),
data_all['transmission'].shape[0]),
dtype=bool)
internal_lines_logic = np.zeros((len(clusters),
data_all['transmission'].shape[0]),
dtype=bool)
for cluster_idx in range(0, len(clusters)):
for j in range(0, data_all['transmission'].shape[0]):
boundarying_lines_logic[cluster_idx, j] = (
(data_all['transmission'].index.get_level_values('Site In')[j]
in clusters[cluster_idx])
^ (data_all['transmission'].index.get_level_values('Site Out')[j]
in clusters[cluster_idx]))
internal_lines_logic[cluster_idx, j] = (
(data_all['transmission'].index.get_level_values('Site In')[j]
in clusters[cluster_idx])
and (data_all['transmission'].index.get_level_values('Site Out')[j]
in clusters[cluster_idx]))
boundarying_lines[cluster_idx] = \
data_all['transmission'].loc[boundarying_lines_logic[cluster_idx, :]]
internal_lines[cluster_idx] = \
data_all['transmission'].loc[internal_lines_logic[cluster_idx, :]]
for i in range(0, boundarying_lines[cluster_idx].shape[0]):
sit_from = boundarying_lines[cluster_idx].iloc[i].name[1]
sit_to = boundarying_lines[cluster_idx].iloc[i].name[2]
for j in timesteps[1:]:
coup_vars.lambdas[cluster_idx, j, year, sit_from, sit_to] = 0
coup_vars.rhos[cluster_idx, j, year, sit_from, sit_to] = 5
coup_vars.flow_global[cluster_idx, j, year, sit_from, sit_to] = 0
In the following optional step, the original problem is built and solved. This is the same as the regular urbs routine, and is used for testing purposes (e.g. comparing the ADMM result against this, making a runtime test). For your actual usage, feel free to comment this section out:
# (optional) create the central problem to compare results
prob = create_model(data_all, timesteps, dt, type='normal')
# refresh time stamp string and create filename for logfile
log_filename = os.path.join(result_dir, '{}.log').format(sce)
# setup solver
solver_name = 'gurobi'
optim = SolverFactory(solver_name) # cplex, glpk, gurobi, ...
optim = setup_solver(optim, logfile=log_filename)
# original problem solution (not necessary for ADMM, to compare results)
orig_time_before_solve = time.time()
results_prob = optim.solve(prob, tee=False)
orig_time_after_solve = time.time()
orig_duration = orig_time_after_solve - orig_time_before_solve
flows_from_original_problem = dict((name, entity.value) for (name, entity) in prob.e_tra_in.items())
flows_from_original_problem = pd.DataFrame.from_dict(flows_from_original_problem, orient='index',
columns=['Original'])
In the next code section, problems
, a list of urbsADMMmodel
Classes and sub
, a dictionary for keeping the Pyomo object of subproblems are initialized. Next, the following steps take place for each region cluster cluster_idx
:
problem
which is an instance of theurbsADMMmodel
class, is initialized (please see the urbsADMMmodel, init Section),- a Pyomo object for the subproblem is created using the
urbs.create_model
function with thetype='sub'
option, See the modified create_model in the model.py changes). This Pyomo instance is stored in the attributesub_pyomo
ofproblem
, - initial values for the global coupling variable values are stored in
problem.flow_global
, which is a subset ofcoup_vars.flow_global
where thecluster_idx
corresponds to the cluster in question, - initial values for the consensus dual variables are stored in
problem.lamda
, which is a subset ofcoup_vars.lambdas
where thecluster_idx
corresponds to the cluster in question, - initial value for the quadratic penalty parameter is stored in
problem.rho
, - the unique index of the cluster is stored in
problem.ID
, - the result directory and the scenario name are stored in the
problem.result_dir
andproblem.sce
respectively, - the
cluster_from
,cluster_to
andneighbor_cluster
columns are appended toboundarying_lines[cluster_idx]
DataFrame using thecalculate_neighbor_cluster_per_line
function. The appended DataFrame is then stored inproblem.boundarying_lines
- the information for the total number of clusters is stored in
problem.na
- the prepared instance
problem
is added to the list ofproblems
problems = [] sub = {} # initiate urbs_admm_model Classes for each subproblem for cluster_idx in range(0, len(clusters)): problem = urbsADMMmodel() sub[cluster_idx] = urbs.create_model(data_all, timesteps, type='sub', sites=clusters[cluster_idx], coup_vars=coup_vars, data_transmission_boun=boundarying_lines[cluster_idx], data_transmission_int=internal_lines[cluster_idx], cluster=cluster_idx) problem.sub_pyomo = sub[cluster_idx] problem.flow_global = {(key[1], key[2], key[3], key[4]): value for (key, value) in coup_vars.flow_global.items() if key[0] == cluster_idx} problem.flow_global = pd.Series(problem.flow_global) problem.flow_global.rename_axis(['t', 'stf', 'sit', 'sit_'], inplace=True) problem.flow_global = problem.flow_global.to_frame() problem.lamda = {(key[1], key[2], key[3], key[4]): value for (key, value) in coup_vars.lambdas.items() if key[0] == cluster_idx} problem.lamda = pd.Series(problem.lamda) problem.lamda.rename_axis(['t', 'stf', 'sit', 'sit_'], inplace=True) problem.lamda = problem.lamda.to_frame() problem.rho = 5 problem.ID = cluster_idx problem.result_dir = result_dir problem.sce = sce boundarying_lines[cluster_idx]['cluster_from'], boundarying_lines[cluster_idx]['cluster_to'], \ boundarying_lines[cluster_idx]['neighbor_cluster'] = calculate_neighbor_cluster_per_line(boundarying_lines, cluster_idx, clusters) problem.boundarying_lines = boundarying_lines[cluster_idx] problem.na = len(clusters) problems.append(problem)
In the next step, queues
are created for each communication channel using the create_queues
function. These are then stored in the respective problem
, along with the following attributes:
neighbors
: the indices of clusters that neighbor the cluster in question,nneighbors
: the number of neighboring clusters,nwait
: the number of neighboring subproblems, that the subproblem has to wait for in order to move on to the next iteration. This is calculated using the productadmmopt.nwaitPercent
ofnneighbors
, rounded up.
edges, queues = create_queues(clusters, boundarying_lines)
# define further necessary fields for the subproblems
for cluster_idx in range(0, len(clusters)):
problems[cluster_idx].neighbors = sorted(set(boundarying_lines[cluster_idx].neighbor_cluster.to_list()))
problems[cluster_idx].nneighbors = len(problems[cluster_idx].neighbors)
problems[cluster_idx].queues = dict((key, value) for (key, value) in queues.items() if key == cluster_idx)
problems[cluster_idx].queues.update(dict(
(key0, {key: value}) for (key0, n) in queues.items() for (key, value) in n.items() if
key == cluster_idx).items())
problems[cluster_idx].nwait = ceil(
problems[cluster_idx].nneighbors * problems[cluster_idx].admmopt.nwaitPercent)
Then, another Queue is created, which is used by each subproblem after they converge to send their solutions:
# define a Queue class for collecting the results from each subproblem after convergence
output = mp.Manager().Queue()
Afterwards, a list (proc
) is initialized, and populated by mp.Process
which take the function run_worker
, to be run for each cluster. The arguments here are:
cluster_idx + 1
: ordinality of the cluster,problems[cluster_idx]
: theurbsADMMmodel
instance corresponding to the cluster,output
: the Queue to be used for sending the subproblem solution
The processes are then launched using the .start()
method.:
# define the asynchronous jobs for ADMM routines
procs = []
for cluster_idx in range(0, len(clusters)):
procs += [mp.Process(target=run_worker, args=(cluster_idx + 1, problems[cluster_idx], output))]
start_time = time.time()
start_clock = time.clock()
for proc in procs:
proc.start()
While the processes are running, attempts to fetch results from output
is made in constant intervals (0.5 seconds by default), until all child processes are finished (while liveprocs:
). A soon as this is the case, we return to the parent thread (proc.join()
):
# collect results as the subproblems converge
results = []
while liveprocs:
try:
while 1:
results.append(output.get(False))
except queue.Empty:
pass
time.sleep(0.5)
if not output.empty():
continue
liveprocs = [p for p in liveprocs if p.is_alive()]
for proc in procs:
proc.join()
Finally, the subproblem results are recovered and compared against the original problem in the following code section:
# ------------get results ---------------------------
ttime = time.time()
tclock = time.clock()
totaltime = ttime - start_time
clocktime = tclock - start_clock
results = sorted(results, key=lambda x: x[0])
obj_total = 0
obj_cent = results_prob['Problem'][0]['Lower bound']
for cluster_idx in range(0, len(clusters)):
if cluster_idx != results[cluster_idx][0]:
print('Error: Result of worker %d not returned!' % (cluster_idx + 1,))
break
obj_total += results[cluster_idx][1]['cost']
gap = (obj_total - obj_cent) / obj_cent * 100
print('The convergence time for original problem is %f' % (orig_duration,))
print('The convergence time for ADMM is %f' % (totaltime,))
print('The convergence clock time is %f' % (clocktime,))
print('The objective function value is %f' % (obj_total,))
print('The central objective function value is %f' % (obj_cent,))
print('The gap in objective function is %f %%' % (gap,))
The run_worker
function (ADMM_async/run_worker.py)¶
In this section, the steps followed by the function run_worker
is explained. This function is run in parallel by each subproblem, and it consists of some initialization steps, ADMM iterations and post-convergence steps.
The function takes three input arguments:
ID
: ordinality of the cluster (1 for the first subproblem, 2 for the second etc.),s
: theurbsADMMmodel
instance corresponding to the cluster,output
: the Queue to be used for sending the subproblem solution
Since ADMM is an iterative method, the subproblems are expected to be solved multiple times (in the order of 10’s, possibly 100’s), with slightly different parameters in each iteration. The pyomo model which defines the optimization problem, first needs to be converted into a lower-level problem formulation (ultimately a set of matrices and vectors), which may take a very long time. Therefore, it is more practical that this conversion step happens only once, and the adjustments between iterations are made on the low-level problem formulation. Pyomo supports the usage of persistent solver interfaces (https://pyomo.readthedocs.io/en/stable/advanced_topics/persistent_solvers.html) for Gurobi, which exactly serves this purpose. These instances are created from the pyomo object with the following code section, and stored in the sub_persistent
attribute:
s.sub_persistent = SolverFactory('gurobi_persistent')
s.sub_persistent.set_instance(s.sub_pyomo, symbolic_solver_labels=False)
Afterwards, the solver parameters can be directly set on the persistent solver instance (Method=2
for barrier method, Thread=1
for allowing the usage of a single CPU):
s.sub_persistent.set_gurobi_param('Method', 2)
s.sub_persistent.set_gurobi_param('Threads', 1)
The .unique()
method is applied to .neighbor_cluster
attribute to retrieve the unique neighbors:
s.neighbor_clusters = s.boundarying_lines.neighbor_cluster.unique()
The local iteration counter nu
is initialized, and the maximum number of iterations maxit
is retrieved from the admmopt
attribute of the subproblem:
nu = 0 # iteration count
maxit = s.admmopt.iterMaxlocal # get maximum iteration
The convergence flag is initialized as False
, the convergence gap as 10**8
and an array keeping track of the objective function value of the solutions as np.zeros
:
s.flag = False
s.gapAll = [10 ** 8] * s.na
cost_history = np.zeros(maxit)
The absolute convergence tolerance is calculated by scaling s.conv_rel
(user input for relative convergence tolerance, set in the admmopt
attribute of the subproblem) with the number of the coupling variables in the subproblem (len(s.flow_global)
, added 1 to ensure convergence for the subproblems without any coupling variables):
s.convergetol = s.conv_rel * (len(s.flow_global)+1) # # convergence criteria for maximum primal gap
Now, the local ADMM iterations take place:
while nu <= maxit-1 and not s.flag:
First, if any message from neighbors is received (if s.recvmsg
is not empty), the global values of the coupling variables are updated (with the .update_z
method), along with choosing the quadratic penalty value that corresponds to the maximum among all the neighbors (with the .choose_max_rho
method):
if s.recvmsg:
s.update_z() # update global flows
s.choose_max_rho() # update choose max rho
Then, to prepare the model for the next run, the updated global values, consensus Lagrange multipliers and penalty parameters are set for the Gurobi instance of the subproblem. For these steps, the methods .fix_flow_global
, .fix_lambda
and set_quad_cost
is applied respectively:
s.fix_flow_global()
s.fix_lambda()
if nu > 0:
s.set_quad_cost(rhos_old)
Now the subproblem can be solved, using the .solve_problem
method:
s.result = s.solve_problem()
After solving the problem, the optimal values of the coupling variables are extracted using the method .retrieve_boundary_flows
. The output of this method are twofold:
s.flows_all
: apd.MultiIndex
containing all the coupling variables,s.flows_with_neighbor
: a dictionary ofpd.MultiIndex``es , whose elements are subsets of ``flows_all
that are shared with a certain neighbor. For instance, for the subproblem with indexs 0, s.flows_with_neighbor[2] will return the values of all coupling variables for the flows between the cluster 0 and 2.
Additionally, the objective value of the optimum is saved in cost_history
:
# retrieve
s.flows_all, s.flows_with_neighbor = s.retrieve_boundary_flows()
cost_history[nu] = s.sub_persistent._solver_model.objval
After obtaining the solutions, the consensus Lagrange multiplier and quadratic penalty parameter is updated with the method .update_y
and the method .update_rho
respectively:
rhos_old = s.rho
if s.recvmsg: # not the initialization
s.update_y() # update lambda
s.update_rho(nu)
Convergence is checked with the .converge
method:
# check convergence
s.flag = s.converge()
At the last step of each iteration, the recvmsg cache is emptied. Afterwards, relevant messages are sent to every neighbor, and are received from neighbors with the .send
method and the .recv
method respectively. For receiving methods, an optional argument pollrounds
can be given. This gives the number of queries made for each message reception per neighbor (default value is 5), and thereby ensures that the message received is as up-to-date as possible.:
s.recvmsg = {} # clear the received messages
s.send()
s.recv(pollrounds=5)
The local iteration counter is updated before moving onto the next iteration:
nu += 1
When the algorithm converges, the final pyomo model of the subproblem and the corresponding solution is saved with the save function:
save(s.sub_pyomo, os.path.join(s.result_dir, '_{}_'.format(ID),'{}.h5'.format(s.sce)))
Additionally, a dictionary consisting of the final objective value, the values of coupling variables and primal/dual residuals is created and put into the Queue
called output
:
output_package = {'cost': cost_history[nu - 1], 'coupling_flows': s.flow_global,
'primal_residual': s.primalgap, 'dual_residual': s.dualgap}
output.put((ID - 1, output_package))
The urbsADMMmodel Class (ADMM_async/urbs_admm_model.py)¶
In this section, the initialization attributes and methods of the urbsADMMmodel
class will be explained. This class is the main argument of the parallel calls of the run_worker
function, encapsulates the local urbs subproblem and implements the ADMM steps including solving the subproblem, sending and recieving data to/from neighbors, updating global values of the coupling variables, the consensus Lagrange multipliers and the quadratic penalty parameters.
While the order in which these ADMM steps are followed is listed in the previous section, here the steps themselves will be described.
Starting with the attributes list of an urbsADMMmodel
instance:
class urbsADMMmodel(object):
def __init__(self):
# initialize all the fields
self.boundarying_lines = None
self.flows_all = None
self.flows_with_neighbor = None
self.flow_global = None
self.sub_pyomo = None
self.sub_persistent = None
self.neighbors = None
self.nneighbors = None
self.nwait = None
self.var = {'flow_global': None, 'rho': None}
self.ID = None
self.nbor = {}
self.pipes = None
self.queues = None
self.admmopt = admmoption()
self.recvmsg = {}
self.primalgap = [9999]
self.dualgap = [9999]
self.gapAll = None
self.rho = None
self.lamda = None
These attributes are described as follows:
self.boundarying_lines
: A pd.MultiIntex, that is a subset of Transmission lines that connect this cluster with other clusters,self.flows_all
: apd.MultiIndex
containing the optimized values of all the coupling variables (Elec
andCarbon
flows) after a subproblem solutionself.flows_with_neighbor
: a dictionary ofpd.MultiIndex``es , whose elements are subsets of ``flows_all
that are shared with a certain neighborself.flow_global
: apd.MultiIndex
containing the global values of all the coupling variables (Elec
andCarbon
flows)self.sub_pyomo
: apyomo.environ.ConcreteModel
object that represents the subproblemself.sub_persistent
: aGurobiPersistent
object, a persistent solver interface on which the model adjustments are madeself.neighbors
: the indices of clusters that neighbor the cluster in questionself.nneighbors
: the number of neighboring clustersself.nwait
: the number of neighboring subproblems, that the subproblem has to wait for in order to move on to the next iteration. This is calculated using the productadmmopt.nwaitPercent
ofnneighbors
, rounded up.self.ID
: the subproblem ID. An integer starting from 0 (for the first subproblem).self.queues
: a dictionary of dictionary ofmp.Manager().Queue()
objects, which has the cluster in question either as the receiving or the sending endself.admmopt
: an instance of theadmmoption
class. These include the ADMM parameters, which can be modified by the user. They will be listed below.self.recvmsg
: an instance of themessage
class. This class is sent and received between the workers, and its attributes will be listed below.self.primalgap
: an array which extends which each iteration, and keeps track of the primal residual of the solutionself.dualgap
: an array which extends which each iteration, and keeps track of the dual residual of the solutionself.gapAll
: a list which includes: primal residual of the subproblem, along with the primal residuals of neighboring clustersself.rho
: a real number which represents the current value of the quadratic penalty parameterself.lamda
: apd.MultiIndex
containing the values of the current consensus Lagrange multipliers
Before explaining the methods of urbsADMMmodel
class, let us have a look at the two auxiliary classes admmoption
and message
:
class admmoption(object):
""" This class defines all the parameters to use in admm """
def __init__(self):
self.rho_max = 10 # upper bound for penalty rho
self.tau_max = 1.5 # parameter for residual balancing of rho
self.tau = 1.05 # multiplier for increasing rho
self.zeta = 1 # parameter for residual balancing of rho
self.theta = 0.99 # multiplier for determining whether to update rho
self.mu = 10 # multiplier for determining whether to update rho
self.pollWaitingtime = 0.001 # waiting time of receiving from one pipe
self.nwaitPercent = 0.2 # waiting percentage of neighbors (0, 1]
self.iterMaxlocal = 20 # local maximum iteration
self.rho_update_nu = 50 # rho is updated only for the first 50 iterations
self.conv_rel = 0.1 # the relative convergece tolerance, to be multiplied with (len(s.flow_global)+1)
The admmoption
class includes numerous parameters that specify the ADMM method, which can be set by the user:
self.rho_max
: A positive real number, that sets an upper bound for the quadratic penalty parameter (see.update_rho
for its usage)self.tau_max
: A positive real number, that sets an upper bound for the per-iteration modifier of the quadratic penalty parameter (see.update_rho
for its usage)self.tau
: A positive real number, that scales the quadratic penalty parameter up or down (see.update_rho
for its usage)self.zeta
: A positive real number, that is used for the residual balancing of the quadratic penalty parameter (not in use currently)self.theta
: A positive real number, that is used for the residual balancing of the quadratic penalty parameter (not in use currently)self.mu
: A positive real number, that is used for the scaling of the quadratic penalty parameter (see.update_rho
for its usage)self.pollWaitingtime
: A positive real number, which represents the waiting time for receiving a message from a neighbor (seerecv
for its usage)self.nwaitPercent
: A real number within (0, 1], that gives the percentage of its neighbors that a subproblem needs to receive a message in order to move onto the next iteration (see line 258 ofrunfunctions_admm.py
for its usage)self.iterMaxlocal
: A positive integer, that sets the maximum number of local iterations (see line 25 ofrun_Worker.py
for its usage)self.rho_update_nu
: A positive integer, that sets the last iteration number where the quadratic penalty parameter is updated. After this iteration number, it will not be updated anymore (see.update_rho
for its usage)self.conv_rel
: A positive real number, that is multiplied with(len(s.flow_global)+1)
to set the absolute convergence tolerance of a local subproblem
Moving onto the message
class:
class message(object):
""" This class defines the message region i sends to/receives from j """
def __init__(self):
self.fID = 0 # source region ID
self.tID = 0 # destination region ID
self.fields = {
'flow': None,
'rho': None,
'lambda': None,
'convergeTable': None}
def config(self, f, t, var_flow, var_rho, var_lambda, gapall): # AVall and var are local variables of f region
self.fID = f
self.tID = t
self.fields['flow'] = var_flow
self.fields['rho'] = var_rho
self.fields['lambda'] = var_lambda
self.fields['convergeTable'] = gapall
Instances of this class are the packets that are communicated between the workers and contain the following attributes:
fID
: the index of the sending subproblemtID
: the index of the receiving subproblemfields
: a dictionary which consists of the exchanged message (the local optimizing values of coupling variablesflow
, the local quadratic parameter valuerho
, the local consensus Lagrange multiplierlambda
and the local primal residualgapall
)
Now let us return to the class urbsADMMmodel
and go through its methods.
.solve_problem
takes the persistent solver interface and solves it with the options save_results
and load_solutions
as False
to save runtime. warmstart
is set as True
, even though the barrier solver does not support this feature yet.:
def solve_problem(self):
self.sub_persistent.solve(save_results=False, load_solutions=False, warmstart=True)
Three following methods (.fix_flow_global
, .fix_lambda
and .set_quad_cost
) interface with the pyomo object and persistent solver interface of the subproblem, and modify the cost function with the updated global values of the coupling variable, consensus Lagrange multiplier and the quadratic penalty parameter. Observe that in the model, lamda
(consensus Lagrange multiplier) and flow_global
(global value of the coupling variable) are defined as Variables
whose values are then fixed in the model with the .fix
method, whereas the quadratic penalty parameter rho
is a real number.:
def fix_flow_global(self):
for key in self.flow_global.index:
if not isinstance(self.flow_global.loc[key], pd.core.series.Series):
self.sub_pyomo.flow_global[key].fix(self.flow_global.loc[key])
self.sub_persistent.update_var(
self.sub_pyomo.flow_global[key])
else:
self.sub_pyomo.flow_global[key].fix(self.flow_global.loc[key, 0])
self.sub_persistent.update_var(
self.sub_pyomo.flow_global[key])
def fix_lambda(self):
for key in self.lamda.index:
if not isinstance(self.lamda.loc[key], pd.core.series.Series):
self.sub_pyomo.lamda[key].fix(self.lamda.loc[key])
self.sub_persistent.update_var(self.sub_pyomo.lamda[key])
else:
self.sub_pyomo.lamda[key].fix(self.lamda.loc[key, 0])
self.sub_persistent.update_var(self.sub_pyomo.lamda[key])
def set_quad_cost(self, rhos_old):
quadratic_penalty_change = 0
# Hard coded transmission name: 'hvac', commodity 'Elec' for performance.
# Caution, as these need to be adjusted if the transmission of other commodities exists!
for key in self.flow_global.index:
if (key[2] == 'Carbon_site') or (key[3] == 'Carbon_site'):
quadratic_penalty_change += 0.5 * (
self.rho - rhos_old) * \
(self.sub_pyomo.e_tra_in[
key, 'CO2_line', 'Carbon'] -
self.sub_pyomo.flow_global[key]) ** 2
else:
quadratic_penalty_change += 0.5 * (
self.rho - rhos_old) * \
(self.sub_pyomo.e_tra_in[key, 'hvac', 'Elec'] -
self.sub_pyomo.flow_global[key]) ** 2
old_expression = self.sub_persistent._pyomo_model.objective_function.expr
self.sub_persistent._pyomo_model.del_component('objective_function')
self.sub_persistent._pyomo_model.add_component('objective_function',
pyomo.Objective(expr = old_expression + quadratic_penalty_change,
sense=pyomo.minimize))
self.sub_persistent.set_objective(
self.sub_persistent._pyomo_model.objective_function)
self.sub_persistent._solver_model.update()
With the methods send
and recv
, the message transfer bwetween subproblems take place. Let us start with send
:
def send(self):
dest = self.queues[self.ID].keys()
for k in dest:
# prepare the message to be sent to neighbor k
msg = message()
msg.config(self.ID, k, self.flows_with_neighbor[k], self.rho,
self.lamda[self.lamda.index.isin(self.flows_with_neighbor[k].index)],
self.gapAll)
self.queues[self.ID][k].put(msg)
The send
method prepares a message
for each neighbor k
, where only the subset of the coupling variable and Lagrange multiplier values which are relevant to this neighbor are sent (self.flows_with_neighbor[k]
and self.lamda[self.lamda.index.isin(self.flows_with_neighbor[k].index)]
). Additionally, the quadratic penalty parameter self.rho
and the local residual gap self.gapAll
is also communicated.
These values are inserted into the message with the .config
method, and the message is sent (put into the Queue
) using the .put
method.
Next, the .recv
method:
def recv(self, pollrounds=5):
twait = self.admmopt.pollWaitingtime
dest = list(self.queues[self.ID].keys())
recv_flag = [0] * self.nneighbors
arrived = 0 # number of arrived neighbors
pollround = 0
# keep receiving from nbor 1 to nbor K in round until nwait neighbors arrived
while arrived < self.nwait and pollround < pollrounds:
for i in range(len(dest)):
k = dest[i]
while not self.queues[k][self.ID].empty(): # read from queue until get the last message
self.recvmsg[k] = self.queues[k][self.ID].get(timeout=twait)
recv_flag[i] = 1
# print("Message received at %d from %d" % (self.ID, k))
arrived = sum(recv_flag)
pollround += 1
The recv
method attempts to receive the message
from at least self.nwait
neighbors. Within the loop for i in range(len(dest))
, the message-reception queue from each neighbor is queried (with the .get
method) until the queue is empty (hence while not self.queues[k][self.ID].empty()
). When the arrived
counter is at least self.nwait
, the .recv
procedure finishes.
Then we come to the three methods that update the global values of the coupling variable (.update_z
), consensus Lagrange multiplier (.update_y
) and the quadratic penalty parameter (.update_rho
). Note that these methods are used to obtain new values for these variables, and their application to the problem takes place afterwards with the methods .fix_flow_global
, .fix_lambda
and .set_quad_cost
as explained earlier.
Starting with update_z
:
def update_z(self):
srcs = self.queues[self.ID].keys()
flow_global_old = deepcopy(self.flow_global)
for k in srcs:
if k in self.recvmsg and self.recvmsg[k].tID == self.ID: # target is this Cluster
nborvar = self.recvmsg[k].fields # nborvar['flow'], nborvar['convergeTable']
self.flow_global.loc[self.flow_global.index.isin(self.flows_with_neighbor[k].index)] = \
(self.lamda.loc[self.lamda.index.isin(self.flows_with_neighbor[k].index)] +
nborvar['lambda'] + self.flows_with_neighbor[k] * self.rho + nborvar['flow'] * nborvar['rho']) \
/ (self.rho + nborvar['rho'])
self.dualgap += [self.rho * (np.sqrt(np.square(self.flow_global - flow_global_old).sum(axis=0)[0]))]
For updating the global variable, a loop is made, checking for each source (neighboring cluster) whether a new message is present that is meant for the cluster in question (self.recvmsg and self.recvmsg[k].tID == self.ID
), and if yes, the global variable is updated using the equation provided in the theoretical section of the documentation. After the global value is updated using information from all sending neighbors, the new value for the dual residual is also calculated.
After updating the global flow value, the Lagrange multiplier update can be made by the update_y
method using the equation provided in the theoretical section of the documentation:
def update_y(self):
self.lamda = self.lamda + self.rho * (self.flows_all.loc[:, [0]] - self.flow_global)
Then the quadratic penalty parameter is updated by the .update_rho
method and then replaced by the maximum quadratic penalty parameter across all neighbors by the .choose_max_rho
method:
# update rho and primal gap locally
def update_rho(self, nu):
self.primalgap += [np.sqrt(np.square(self.flows_all - self.flow_global).sum(axis=0)[0])]
# update rho (only in the first rho_iter_nu iterations)
if nu <= self.admmopt.rho_update_nu:
if self.primalgap[-1] > self.admmopt.mu * self.dualgap[-1]:
self.rho = min(self.admmopt.rho_max, self.rho * self.admmopt.tau)
elif self.dualgap[-1] > self.admmopt.mu * self.primalgap[-1]:
self.rho = min(self.rho / self.admmopt.tau, self.admmopt.rho_max)
# update local converge table
self.gapAll[self.ID] = self.primalgap[-1]
# # use the maximum rho among neighbors for local update
def choose_max_rho(self):
srcs = self.recvmsg.keys()
for k in srcs:
rho_nbor = self.recvmsg[k].fields['rho']
self.rho = maximum(self.rho, rho_nbor) # pick the maximum one
Whether the quadratic penalty parameter has to increase or decrease depends on the relation between primalgap
and dualgap
, admmopt.tau
, admmopt.tau_max
admmopt.rho_max
and admmopt.tau_max
. Therefore, before updating rho
, the primal residual primalgap
is also calculated within this method. For a mathematical description of the rho
update, please refer to page 20 of https://stanford.edu/class/ee367/reading/admm_distr_stats.pdf.
The convergence is checked with the method .converge
:
def converge(self):
# first update local converge table using received converge tables
if self.recvmsg is not None:
for k in self.recvmsg:
table = self.recvmsg[k].fields['convergeTable']
self.gapAll = list(map(min, zip(self.gapAll, table)))
# check if all local primal gaps < tolerance
if max(self.gapAll) < self.convergetol:
return True
else:
return False
Here, a convergence table is updated (or created, in case the first iteration) which consists of the primal residuals of all the neighboring subproblems and the subproblem in question itself (self.gapAll = list(map(min, zip(self.gapAll, table)))
). If all of these local primal residuals are smaller than the absolute tolerance (max(self.gapAll) < self.convergetol
), the method returns a True
, and False
otherwise.
The last method defined for urbsADMMmodel
is retrieve_boundary_flows
:
def retrieve_boundary_flows(self):
e_tra_in_per_neighbor = {}
self.sub_persistent.load_vars(self.sub_pyomo.e_tra_in[:, :, :, :, :, :])
boundary_lines_pairs = self.boundarying_lines.reset_index().set_index(['Site In', 'Site Out']).index
e_tra_in_dict = {(tm, stf, sit_in, sit_out): v.value for (tm, stf, sit_in, sit_out, tra, com), v in
self.sub_pyomo.e_tra_in.items() if ((sit_in, sit_out) in boundary_lines_pairs)}
e_tra_in_dict = pd.DataFrame(list(e_tra_in_dict.values()),
index=pd.MultiIndex.from_tuples(e_tra_in_dict.keys())).rename_axis(
['t', 'stf', 'sit', 'sit_'])
for (tm, stf, sit_in, sit_out) in e_tra_in_dict.index:
e_tra_in_dict.loc[(tm, stf, sit_in, sit_out), 'neighbor_cluster'] = self.boundarying_lines.reset_index(). \
set_index(['support_timeframe', 'Site In', 'Site Out']).loc[(stf, sit_in, sit_out), 'neighbor_cluster']
for neighbor in self.neighbors:
e_tra_in_per_neighbor[neighbor] = e_tra_in_dict.loc[e_tra_in_dict['neighbor_cluster'] == neighbor]
e_tra_in_per_neighbor[neighbor].reset_index().set_index(['t', 'stf', 'sit', 'sit_'], inplace=True)
e_tra_in_per_neighbor[neighbor].drop('neighbor_cluster', axis=1, inplace=True)
return e_tra_in_dict, e_tra_in_per_neighbor
This method loads the optimized flow variables from the model solution (self.sub_persistent.load_vars(self.sub_pyomo.e_tra_in[:, :, :, :, :, :])
), and then applies to it a series of pd.DataFrame
operations to produce the necessary data structures.
Changes made in the create_model
function (model.py)¶
In the ADMM implementation, several adjustments were made in the model creation, for the specific case of creating the subproblems. Therefore, the create_model
function now takes several additional optional input arguments:
def create_model(data_all, timesteps=None, dt=1, objective='cost', dual=False, type='normal', sites = None, coup_vars=None, data_transmission_boun=None, data_transmission_int=None, cluster=None):
Here, the type=='sub'
specifies the case of creating a subproblem, sites
are the model regions contained by the given cluster, coup_vars
are the initialized values of the global flow values, data_transmission_boun
and data_transmission_int
are the data sets of transmission lines which include the intercluster and internal lines that are present for the considered subproblem, cluster
is the index of the considered subproblem.
In the following, only the changes made on the create_model
function for the ADMM implementation are mentioned.
The model preperation function pyomo_model_prep
takes the model type
as an argument, and creates a subset of the whole data structure data_all
which is then passed to data
:
if type == 'sub':
m, data = pyomo_model_prep(data_all, timesteps, sites, type,
pd.concat([data_transmission_boun,data_transmission_int])) # preparing pyomo model
Note
Changes made in the ``pyomo_model_prep`` function (input.py, line 185)
In case the model type is sub
, the cross-sections of the whole data structure which contains the specificed sites
are taken:
data = deepcopy(data_all)
m.timesteps = timesteps
data['site_all']=data_all['site']
if type =='sub':
m.global_prop = data_all['global_prop'].drop('description', axis=1)
data['site'] = data_all['site'].loc(axis=0)[:,sites]
data['commodity'] = data_all['commodity'].loc(axis=0)[:,sites]
data['process'] = data_all['process'].loc(axis=0)[:,sites]
data['storage'] = data_all['storage'].loc(axis=0)[:,sites]
if sites != ['Carbon_site']:
data['demand'] = data_all['demand'][sites]
data['supim']= data_all['supim'][sites]
else:
data['demand'] = pd.DataFrame()
data['supim'] = pd.DataFrame()
data['transmission'] = data_transmission
Returning to create_model
, in case the model type is sub
, the quadratic penalty parameter rho
is specified as the value that corresponds to the cluster in question:
if m.type =='sub':
rho = dict((key[1:],value) for key, value in coup_vars.rhos.items() if key[0] == cluster)
which is then set as a pyomo.environ.Parameter
, along with flow_global
(global values of coupling variables) and lamda
(consensus Lagrange multipliers) as ``pyomo.environ.Variable``s:
if type=='sub':
m.flow_global = pyomo.Var(
m.tm,m.stf,m.sit,m.sit,
within=pyomo.Reals,
doc='flow global in')
m.lamda = pyomo.Var(
m.tm,m.stf,m.sit,m.sit,
within=pyomo.Reals,
doc='lambda in')
m.rho = pyomo.Param(
m.tm,m.stf,m.sit,m.sit,
initialize=rho,
doc='rho in')
In ADMM, the objective function is adjusted by the linear and quadratic penalty terms. This is implemented via the following lines:
def cost_rule(m):
if m.type =='sub':
return (pyomo.summation(m.costs) + sum(0.5 * m.rho[(tm, stf, sit_in, sit_out)] *
(m.e_tra_in[(tm, stf, sit_in,sit_out, tra, com)]
-m.flow_global[(tm, stf, sit_in,sit_out)])**2
for tm in m.tm
for stf, sit_in, sit_out, tra, com in m.tra_tuples_boun) + sum(m.lamda[(tm, stf, sit_in, sit_out)] *
(m.e_tra_in[(tm,stf, sit_in,sit_out,tra,com)]
-m.flow_global[(tm, stf, sit_in,sit_out)])
for tm in m.tm
for stf, sit_in, sit_out, tra, com in m.tra_tuples_boun) )
In urbs, the transmission line capacities are built twice (once in both directions). Therefore, a halving of the investment and fixed costs has to be made in the pre-processing part of the data input. However, when the subsystems are decomposed, we have to introduce a further halving of the intercluster transmission lines, so that we avoid both clusters having to pay for this line twice as this would disrupt the costs of the whole system Therefore, the system costs m.costs
are also defined with a slight difference:
elif m.type == 'sub':
m.def_costs = pyomo.Constraint(
m.cost_type,
rule=def_costs_rule_sub,
doc='main cost function by cost type')
One can see that the cost rule differs in name (def_costs_rule_sub
). In this adjusted rule, the transmission costs are called via the function transmission_cost_sub
instead of transmission_costs
. This function is located in urbs/features/transmission.py
at line 429 (note the coefficients 0.5
)
def transmission_cost_sub(m, cost_type):
"""returns transmission cost function for the different cost types"""
if cost_type == 'Invest':
cost = (sum(m.cap_tra_new[t] *
m.transmission_dict['inv-cost'][t] *
m.transmission_dict['invcost-factor'][t]
for t in m.tra_tuples - m.tra_tuples_boun)
+ 0.5 * sum(m.cap_tra_new[t] *
m.transmission_dict['inv-cost'][t] *
m.transmission_dict['invcost-factor'][t]
for t in m.tra_tuples_boun))
if m.mode['int']:
cost -= (sum(m.cap_tra_new[t] *
m.transmission_dict['inv-cost'][t] *
m.transmission_dict['overpay-factor'][t]
for t in m.tra_tuples_internal)
+ 0.5 * sum(m.cap_tra_new[t] *
m.transmission_dict['inv-cost'][t] *
m.transmission_dict['overpay-factor'][t]
for t in m.tra_tuples_boun))
return cost
elif cost_type == 'Fixed':
return (sum(m.cap_tra[t] * m.transmission_dict['fix-cost'][t] *
m.transmission_dict['cost_factor'][t]
for t in m.tra_tuples_internal)
+ 0.5 * sum(m.cap_tra[t] * m.transmission_dict['fix-cost'][t] *
m.transmission_dict['cost_factor'][t]
for t in m.tra_tuples_boun))
elif cost_type == 'Variable':
if m.mode['dpf']:
return (sum(m.e_tra_in[(tm,) + t] * m.weight *
m.transmission_dict['var-cost'][t] *
m.transmission_dict['cost_factor'][t]
for tm in m.tm
for t in m.tra_tuples_tp) + \
sum(m.e_tra_abs[(tm,) + t] * m.weight *
m.transmission_dict['var-cost'][t] *
m.transmission_dict['cost_factor'][t]
for tm in m.tm
for t in m.tra_tuples_dc))
else:
return (sum(m.e_tra_in[(tm,) + t] * m.weight *
m.transmission_dict['var-cost'][t] *
m.transmission_dict['cost_factor'][t]
for tm in m.tm
for t in m.tra_tuples_internal)
+ 0.5 * sum(m.e_tra_in[(tm,) + t] * m.weight *
m.transmission_dict['var-cost'][t] *
m.transmission_dict['cost_factor'][t]
for tm in m.tm
for t in m.tra_tuples_boun))
This concludes the documentation of the ADMM implementation on urbs.