ADMM user guide

This section serves as a guide for those who would like to use the regional decomposition module by ADMM.

Setting the modelled time steps

As with the usual urbs, the modelled time steps has to be set on runme_admm.py in the corresponding line

Clustering scheme for the regional decomposition

Regional decomposition only makes sense if the energy system model contains multiple sites. These sites then need to be assigned to different subproblems in “clusters”, whose scheme has to be input on runme_admm.py within the variable clusters in the corresponding line:

clusters = [[('site 1 of cluster 1'),('site 2 of cluster 1'),('site 3 of cluster 1'')],
            [('site 1 of cluster 2'),('site 2 of cluster 2')]]

Any number of clusters is possible, from two to the total number of sites (each site forming its own cluster). For the trivial case of having only a single cluster, the regional decomposition is obviously not necessary.

The input of ADMM parameters

The initialized values of ADMM parameters can be set in the following line on the runfunctions_admm.py script:

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

as well as here again for the quadratic penalty parameter:

problem.rho = 5

ADMM settings (admmoption)

Lastly, the ADMM settings, which are input as attributes of the class admmoption of urbsADMMmodel can be fine tuned depending on the problem type. These settings can be found in the corresponding section of ADMM_async/urbs_admm_model.py:

# ##--------ADMM parameters specification -------------------------------------
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.convergetol = 365 * 10 ** 1#  convergence criteria for maximum primal gap
        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)

Commenting out the original problem solution

The runfunctions_admm.py includes the routines for building and solution of the original, undecomposed model for testing purposes. When the problem is solved in a decomposed way, the original problem doesn’t need to be solved. Therefore, the following code section has to be commented out in actual operation:

# (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'])

as well as the test procedure at the end of runfunctions_admm.py:

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