├── DDSG ├── DDSG.py └── __init__.py ├── IRBC ├── IRBC.py └── __init__.py ├── LICENSE ├── README.md ├── examples ├── analytical │ ├── tutorial.ipynb │ └── unit_test.py └── irbc │ ├── tutorial.ipynb │ └── unit_test.py ├── requirements.txt └── setup.py /DDSG/DDSG.py: -------------------------------------------------------------------------------- 1 | ## DDSG Class for Function approxmation 2 | """ 3 | This code supplements the work of Eftekhari and Scheidegger titled High-Dimensional Dynamic Stochastic Model Representation, in SIAM Journal on Scientific 4 | Computing (SISC), 2022 which introduces DDSG, a highly scalable function approximation technique. The lightweight (MPI parallel) Python implementation 5 | presented here is intended showcase its applicability practical. 6 | 7 | The DDSG technique is grid based function approximation method which combines a variant of Dimensional Decomposition known as High-Dimension Model 8 | Representation (HDMR), and adaptive Sparse Grid. The combined approach allows for a highly performant and scalable gird base function approximation method 9 | which can scale to high-dimensions. 10 | 11 | Please cite this paper in your publications if it helps your research: 12 | 13 | Eftekhari, Aryan and Scheidegger, Simon (2022); High-Dimensional Dynamic Stochastic Model Representation 14 | @article{Eftekhari_Scheidegger_2022, 15 | title={High-Dimensional Dynamic Stochastic Model Representation}, 16 | author={Eftekhari, Aryan and Scheidegger, Simon}, 17 | journal={forthcoming in the SIAM Journal on Scientific Computing - Available at SSRN 3603294}, 18 | year={2022} 19 | } 20 | """ 21 | 22 | import Tasmanian 23 | from mpi4py import MPI 24 | import numpy as np 25 | from itertools import combinations 26 | import os 27 | import uuid 28 | import time 29 | import dill 30 | 31 | class DDSG: 32 | 33 | def __init__(self,folder_name:str=None): 34 | """Constructor of DDSG class 35 | 36 | Args: 37 | folder_name (str, optional): Folder name to load a ddsg dump. Defaults to None. 38 | """ 39 | 40 | if folder_name==None: 41 | pass 42 | else: 43 | with open(folder_name+'/ddsg', 'rb') as handle: 44 | obj=dill.load(handle) 45 | 46 | self.__dict__ = obj.__dict__ 47 | 48 | self.proc_comm = MPI.COMM_WORLD.Dup() 49 | self.proc_size = obj.proc_comm.Get_size() 50 | self.proc_rank = obj.proc_comm.Get_rank() 51 | 52 | for u in self.sg_obj: 53 | if len(u)!=0: 54 | self.sg_obj[u] = Tasmanian.SparseGrid() 55 | self.sg_obj[u].read(self._folder_sg_path(folder_name,u)) 56 | 57 | def init(self,f_orical:object, d:int, m:int=1): 58 | """Initilize DDSG object 59 | 60 | Args: 61 | f_orical (object): Scalar valued function. 62 | d (int): Dimension of grid. 63 | m (int,optional): Degrees of freedom (Defual =1). 64 | """ 65 | 66 | assert d>1, 'd must be greater than 1.' 67 | 68 | np.random.seed(1) 69 | self.zero = np.finfo(np.float64).eps * 2 70 | 71 | # f_orical : R^d -> R^k , with 72 | # x0 is the anchor points 73 | # S = {1,2,...,d} 74 | self.d = d 75 | self.m = m 76 | self.f_orical = f_orical 77 | self.S = range(1,self.d+1) 78 | self.x0 = None 79 | 80 | # Flags 81 | self._flag_no_hdmr = False 82 | self._flag_grid_set = False 83 | self._flag_decomposition_set = False 84 | self._flag_build_complete = False 85 | 86 | # Variable defintions see _reset_data_structures() 87 | self.hdmr_obj = None 88 | self.sg_obj = None 89 | self.settings = {} 90 | self.X_sample = None 91 | 92 | # Other Information 93 | self.num_grid_points = None 94 | self.num_func = None 95 | 96 | # MPI world communicator 97 | self.proc_comm = MPI.COMM_WORLD.Dup() 98 | self.proc_size = self.proc_comm.Get_size() 99 | self.proc_rank = self.proc_comm.Get_rank() 100 | 101 | # MPI groups 102 | self.proc_group_number = None 103 | self.proc_group_comm = None 104 | self.proc_group_size = None 105 | self.proc_group_rank = None 106 | self.proc_group_count = None 107 | 108 | # Every rank has the same id 109 | self.id = uuid.uuid4().hex 110 | self.id = self.proc_comm.bcast(self.id, root=0) 111 | 112 | # Temporary folder name 113 | self.temp_folder = os.getcwd()+"/__TEMP_DDSG__"+str(self.id) 114 | 115 | # Second layer of parallelization in SG 116 | self.sg_prl=False 117 | 118 | self._reset_data_structures() 119 | 120 | def set_grid(self,l_max:int,domain:np.array,eps_sg:float,grid_order:int=1,grid_rule:str='localp'): 121 | """Set adaptive SG paramters, based on TASMANIAN. See https://github.com/ORNL/TASMANIAN for further details. 122 | 123 | Args: 124 | domain (np.array): The domain to be approximated, a (d,2) matrix. 125 | l_max (int): Maximum refinment level of SG. 126 | eps_sg (float): Adaptive SG tolerance. 127 | grid_order (int, optional): An integer no smaller than -1 indicating the largest polynomial order of the basis functions (see TASMANIAN grid_order). Defaults to 1. 128 | grid_rule (str, optional): Local polynomial rules (see TASMANIAN grid_rule). Defaults to 'localp'. 129 | """ 130 | 131 | assert domain.shape==(self.d,2), 'domain must have shape (d,2).' 132 | 133 | for i in range(self.d): 134 | assert domain[i,1] > domain[i,0], 'domain limits bust be such that domain[i,1]>domain[i,0] for all i' 135 | 136 | assert l_max>0 , 'l_max must be great than 0.' 137 | assert eps_sg>=0 , 'eps_sg must be great than or equal to 0. ' 138 | assert grid_order >=-1, 'grid_order must b greater or equal to -1.' 139 | 140 | self.settings['domain'] = domain 141 | self.settings['l_max'] = l_max 142 | self.settings['eps_sg'] = max(eps_sg,self.zero) 143 | self.settings['grid_order'] = grid_order 144 | self.settings['grid_rule'] = grid_rule 145 | self.settings['l_start'] = min(1,l_max) 146 | 147 | self._flag_grid_set = True 148 | self._flag_build_complete = False 149 | self._flag_no_hdmr = True 150 | 151 | def set_decomposition(self,x0:np.array,k_max:int,eps_rho:float=0,eps_eta:float=0,N_samples:int=1000): 152 | """Set DD parmeters. If not set, DDSG acts as MPI parallel wrapper for TASMANIAN. 153 | 154 | Args: 155 | x0 (np.array): Achnor point with dimension d. 156 | k_max (int): Maximum expansion order. 157 | eps_rho (float, optional): Convergence criterion tolerance. Defaults to 0. 158 | eps_eta (float, optional): Active dimension selection tolerance. Defaults to 0. 159 | N_samples (int, optional): Number of samples for approximate quadrature. Defaults to 1000. 160 | """ 161 | 162 | # set achor point 163 | 164 | 165 | assert x0.shape ==(1,self.d) ,'x0, must be of size 1xd, but it is'+str(x0.shape)+'.' 166 | assert k_max> 0 ,'k_max must be greater than 0.' 167 | assert eps_rho>=0 ,'eps_rho must be great than or equal to 0.' 168 | assert eps_eta>=0 ,'eps_eta must be great than or equal to 0.' 169 | assert N_samples>1 ,'N_samples must be great than 1.' 170 | 171 | # set anchor point 172 | self.x0 = x0 173 | 174 | # Global sync of random sample need in HDMR 175 | self.X_sample = np.empty(shape=(N_samples,self.d)) 176 | if self.proc_rank==0: 177 | self.X_sample = np.random.uniform(low=self.settings['domain'][:,0], high=self.settings['domain'][:,1], size=(N_samples,self.d)) 178 | self.X_sample = self.proc_comm.bcast(self.X_sample,root=0) 179 | 180 | # level before which we do not adapativity 181 | self.settings['k_max'] = min(k_max,self.d) 182 | self.settings['eps_rho'] = max(eps_rho,self.zero) 183 | self.settings['eps_eta'] = max(eps_eta,self.zero) 184 | 185 | self._flag_decomposition_set = True 186 | self._flag_build_complete = False 187 | self._flag_no_hdmr = False 188 | 189 | def build(self,verbose:int=0): 190 | """Build the interpolant. 191 | 192 | Args: 193 | verbose (int, optional): Display runtime information 0,1,2,3,4 and 99. Defaults to 0. 194 | """ 195 | # error check etc... 196 | assert self._flag_grid_set , 'grid is not set, use .set_grid(...)' 197 | if not self._flag_no_hdmr: # Using hdmr 198 | assert self._flag_decomposition_set, 'decomposition is not set, use .set_decomposition(...)' 199 | 200 | # Load SG setting ... they are need for both methods 201 | l_max = self.settings['l_max'] 202 | l_start = self.settings['l_start'] 203 | eps_sg = self.settings['eps_sg'] 204 | grid_order = self.settings['grid_order'] 205 | grid_rule = self.settings['grid_rule'] 206 | 207 | # reset all data structures 208 | self._reset_data_structures() 209 | 210 | # Compute pure SG 211 | if self._flag_no_hdmr: 212 | 213 | t = time.time() 214 | 215 | if verbose>0 and self.proc_rank==0: print('### SG (sg_prl=%s): d=%d m=%d l_max=%d ϵ_sg=%0.2e '%(self.sg_prl,self.d,self.m,l_max,eps_sg)) 216 | 217 | # reallocate resrouces 218 | self._set_group_comm(num_tasks=1,verbose=verbose) 219 | 220 | # generate Sparse Grid over the full dimensional space 221 | u=(-1,) 222 | active_inx=list(range(0,self.d)) 223 | sg = self._make_sparse_grid(l_start,l_max,eps_sg,active_inx,grid_order,grid_rule,verbose) 224 | self.num_grid_points += sg.getNumPoints() 225 | self.num_func += 1 226 | self.sg_obj[u] = sg 227 | 228 | if verbose>1 and self.proc_rank==0: print('- Building (Adaptive) Sparse Grid (%0.2e sec.)'%(time.time()-t)) 229 | 230 | self._flag_build_complete=True 231 | 232 | # compute HDMR+SG 233 | else: 234 | 235 | # Load DDSG setting ... they dont exist if we have ASG 236 | k_max = self.settings['k_max'] 237 | eps_rho = self.settings['eps_rho'] 238 | eps_eta = self.settings['eps_eta'] 239 | 240 | if verbose>0 and self.proc_rank==0: print('### DDSG (sg_prl=%s): d=%d m=%d k_max=%d l_max=%d ϵ_sg=%.2e ϵ_ρ=%.2e ϵ_η=%.2e '%(self.sg_prl,self.d,self.m,k_max,l_max,eps_sg,eps_rho,eps_eta)) 241 | 242 | # zeroth expansion order 243 | t = time.time() 244 | u = () 245 | 246 | f0 = self.f_orical(self.x0) 247 | if np.isscalar(f0): 248 | f0 = np.array([f0]) 249 | else: 250 | f0 = f0.reshape(self.m) 251 | 252 | f0_quad = f0 253 | 254 | self.sg_obj[u] = f0 255 | self.hdmr_obj['cfunc_quad'][u] = f0_quad 256 | 257 | self.hdmr_obj['eta'][u] = np.linalg.norm(f0_quad) 258 | self.hdmr_obj['quad_k'].append(np.linalg.norm(f0_quad)) 259 | 260 | self.num_grid_points += 1 261 | self.num_func += 1 262 | 263 | if verbose>1 and self.proc_rank==0: print('- Expansion k=0 of %d continue (%0.2e sec.)'% (k_max,time.time()-t)) 264 | 265 | # make the temporary __TEMP_DDSG__ folder ... 266 | self._folder_make(self.temp_folder) 267 | 268 | # All higher order expansion orders 269 | for k in range(1,k_max+1): 270 | 271 | t = time.time() 272 | 273 | # currrent full index set 274 | U_k = list(combinations(self.S,k)) 275 | 276 | # reallocate compute resources 277 | self._set_group_comm(num_tasks=len(U_k),verbose=verbose) 278 | 279 | cfunc_quad_temp=[] 280 | eta_temp=[] 281 | u_temp=[] 282 | z_temp=[] 283 | 284 | for i, u in enumerate(U_k): 285 | 286 | # round-robbin work allocation on COMM_GROUP 287 | if self.proc_group_number == i%self.proc_group_count: 288 | 289 | # SG approximation of the subsbase spanned by the basis indicies u 290 | # if u_temp_loc is None thatn the SG is dropped 291 | [sg_temp_loc,cfunc_quad_temp_loc,eta_temp_loc,u_temp_loc,z_temp_loc]=self._compute_sg_func(u=u,verbose=verbose) 292 | 293 | if self.proc_group_rank==0: 294 | if u_temp_loc is not None: 295 | # write the SG to file 296 | # we cannot share this object (Tasmanian is a shared library written in c++) 297 | sg_temp_loc.write(self._folder_sg_path(self.temp_folder,u)) 298 | 299 | cfunc_quad_temp.append(cfunc_quad_temp_loc) 300 | eta_temp.append(eta_temp_loc) 301 | u_temp.append(u_temp_loc) 302 | else: 303 | z_temp.append(z_temp_loc) 304 | 305 | 306 | # global sync of relevant data, implicit barrier 307 | # data is index by the rank of the process 308 | # note SG is not shared here, but rather read/loaded from disk 309 | block_container = [] 310 | block_container.append(cfunc_quad_temp) 311 | block_container.append(eta_temp) 312 | block_container.append(u_temp) 313 | block_container.append(z_temp) 314 | 315 | block_container_nested=self.proc_comm.allgather(block_container) 316 | 317 | # remove the nested structures, but dont conver to array 318 | cfunc_quad_temp=[] 319 | eta_temp=[] 320 | u_temp=[] 321 | z_temp=[] 322 | 323 | for block_container in block_container_nested: 324 | cfunc_quad_temp += block_container[0] 325 | eta_temp += block_container[1] 326 | u_temp += block_container[2] 327 | z_temp += block_container[3] 328 | 329 | # assemble accepted values 330 | cfunc_quad_sum_k=0.0 331 | for i,u in enumerate(u_temp): 332 | 333 | # load the SG from disk than delete the file after 334 | self.sg_obj[u] = Tasmanian.SparseGrid() 335 | self.sg_obj[u].read(self._folder_sg_path(self.temp_folder,u)) 336 | 337 | self.hdmr_obj['cfunc_quad'][u] = cfunc_quad_temp[i] 338 | 339 | cfunc_quad_sum_k += cfunc_quad_temp[i] 340 | 341 | self.hdmr_obj['eta'][u] = eta_temp[i] 342 | self.num_grid_points += self.sg_obj[u].getNumPoints() 343 | self.num_func += 1 344 | 345 | # update reject set 346 | self.hdmr_obj['Z_list']+=z_temp 347 | 348 | # current approximate quadrature at expansion order k 349 | self.hdmr_obj['quad_k'].append( self.hdmr_obj['quad_k'][k-1] + cfunc_quad_sum_k ) 350 | 351 | # expanion criterion - | quad_k - quad_(k-1) | / | quad_k - quad_(k-1) | 352 | # small number self.zero to previent numerical issues 353 | rho = np.linalg.norm( cfunc_quad_sum_k )/ (self.zero+np.linalg.norm( self.hdmr_obj['quad_k'][k-1] ) ) 354 | self.hdmr_obj['rho'].append(rho) 355 | 356 | if rho<=eps_rho: 357 | if verbose>1 and self.proc_rank==0: print('- Expansion k=%d of %d truncated ρ=%0.2e≤%0.2e (%0.2e sec.)'%(k,k_max,rho,eps_rho,time.time()-t)) 358 | break 359 | elif k==k_max : 360 | if verbose>1 and self.proc_rank==0: print('- Expansion k=%d of %d end ρ=%0.2e (%0.2e sec.)'%(k,k_max,rho,time.time()-t)) 361 | break 362 | else: 363 | if verbose>1 and self.proc_rank==0: print('- Expansion k=%d of %d continue ρ=%0.2e>%0.2e (%0.2e sec.)'%(k,k_max,rho,eps_rho,time.time()-t)) 364 | 365 | # compute coefficent of sg 366 | lookup_index = list(self.sg_obj.keys()) 367 | self.hdmr_obj['vec_coeff'] = np.zeros(len(lookup_index)) 368 | 369 | for inx,u in enumerate(lookup_index): 370 | len_u = len(u) 371 | # v \subseteq u for r={|u|,|u|-1,...,0} 372 | for k in range(0,len(u)+1): 373 | V = list(combinations(u,k)) 374 | len_u_less_len_v = len_u - k 375 | for v in V: 376 | if v in lookup_index: 377 | self.hdmr_obj['vec_coeff'][lookup_index.index(v)] += np.power(-1,len_u_less_len_v) 378 | 379 | # decomposition Complete ... 380 | # remove __TEMP_DDSG__ folder ... 381 | self._folder_remove(self.temp_folder) 382 | self._flag_build_complete=True 383 | 384 | def eval(self,X:np.array)->np.array: 385 | """Evaluate the interpolant at the point(s) X, with every row being coordinate. 386 | 387 | Args: 388 | X (np.array): An N by d matrix, where N is the number of points and d is the dimension input vector. 389 | 390 | Returns: 391 | np.array: An array of interpolant values. 392 | """ 393 | 394 | X=np.array(X) 395 | [N,d] = X.shape 396 | 397 | # Error checks 398 | assert self._flag_build_complete , 'Interplant has not be build, use .build' 399 | assert d == self.d, "Input dimension not correct." 400 | 401 | if self._flag_no_hdmr : # if pure SG 402 | u=(-1,) 403 | Y = self.sg_obj[u].evaluateBatch(X) 404 | 405 | else: # if hdmr with sg 406 | lookup_index = list(self.sg_obj.keys()) 407 | Y = np.zeros(shape=(N,self.m)) 408 | 409 | for i,u in enumerate(lookup_index): 410 | 411 | if self.hdmr_obj['vec_coeff'][i]==0: 412 | continue 413 | k = len(u) 414 | 415 | if(k==0): 416 | Y += self.sg_obj[u] * self.hdmr_obj['vec_coeff'][i] 417 | else: 418 | #extract partial 419 | active_inx = np.array(u)-1 420 | X_partial = X[:,active_inx].reshape((N,k)) 421 | 422 | # evaluate specific points 423 | Y += self.sg_obj[u].evaluateBatch(X_partial) * self.hdmr_obj['vec_coeff'][i] 424 | return Y 425 | 426 | def get_points_values(self)->list: 427 | """Returns the grid points and function values used in the approximation. 428 | 429 | Returns: 430 | list[np.array,np.array]: A list of grid points and corresponding values. 431 | """ 432 | 433 | #Error checks 434 | assert self._flag_build_complete , 'Interplant has not be fit, use .fit' 435 | 436 | X_values = np.empty(0) 437 | Y_values = np.empty(0) 438 | 439 | if self._flag_no_hdmr: 440 | u=(-1,) 441 | X_values = self.sg_obj[u].getPoints() 442 | Y_values = self.sg_obj[u].getLoadedValues() 443 | else: 444 | lookup_index = list(self.sg_obj.keys()) 445 | 446 | #for key, obj in self.sg_obj.items(): 447 | for u in lookup_index: 448 | 449 | if len(u)==0: 450 | X_values = self.x0 451 | Y_values = self.sg_obj[u] 452 | else: 453 | active_inx = np.array(u)-1 454 | # get poinst from SG and mask them x0 455 | num_grid_points = self.sg_obj[u].getNumPoints() 456 | X_mask = np.tile(self.x0,(num_grid_points,1)) 457 | X_mask[:,active_inx] = self.sg_obj[u].getPoints() 458 | X_values = np.vstack((X_values,X_mask)) 459 | 460 | # get points from SG and mask them x0 461 | Y_values = np.vstack((Y_values,self.sg_obj[u].getLoadedValues())) 462 | 463 | return [X_values,Y_values] 464 | 465 | def benchmark(self,N:int,verbose:int=0)->list: 466 | """Basic unit test for accurecy and runtime. 467 | 468 | Args: 469 | N (int): Number of samples used in the test. 470 | verbose (int, optional): Display runtime information 0,1,2,3,4 and 99. Defaults to 0. 471 | 472 | Returns: 473 | list[float,float,int,int,float,float,float]: Results of benchmark: Max error, L2 error, number of grid points, number of component functions, the time needed to build the approximation, average time of interpolant evaluation, average time for calling the oracle function. 474 | """ 475 | 476 | X = np.empty(shape=(N,self.d)) 477 | if self.proc_rank==0: 478 | X = np.random.uniform(low=self.settings['domain'][:,0], high=self.settings['domain'][:,1], size=(N,self.d)) 479 | X = self.proc_comm.bcast(X,root=0) 480 | 481 | t_orical_single = -time.time() 482 | Y_orical = self.f_orical(X) 483 | t_orical_single += time.time() 484 | t_orical_single = t_orical_single/N 485 | 486 | t_build = -time.time() 487 | self.build(verbose=verbose) 488 | t_build += time.time() 489 | 490 | t_eval = -time.time() 491 | Y_interp = self.eval(X).reshape(Y_orical.shape) 492 | t_eval += time.time() 493 | 494 | err_diff = (Y_orical -Y_interp) 495 | err_l2 = np.linalg.norm(err_diff) / np.linalg.norm(Y_orical) 496 | err_max = np.max(abs(err_diff)) 497 | 498 | # The runtime differ slightly between each node, we take the average 499 | t_orical_single = self.proc_comm.allreduce(t_orical_single,MPI.SUM)/self.proc_size 500 | t_build = self.proc_comm.allreduce(t_build,MPI.SUM)/self.proc_size 501 | t_eval = self.proc_comm.allreduce(t_eval,MPI.SUM)/self.proc_size 502 | 503 | return [float(err_max),float(err_l2),int(self.num_grid_points),int(self.num_func),float(t_build),float(t_eval),float(t_orical_single)] 504 | 505 | def dump(self,folder_name:str,replace=False): 506 | """Dump the DDSG object to file. 507 | 508 | Args: 509 | folder_name (str): Name of folder to store the DDSG dump files. 510 | replace (bool, optional): Overwrite folder if exists. Defaults to False. 511 | """ 512 | 513 | self.proc_comm.barrier() 514 | if self.proc_rank==0: 515 | 516 | if replace==True: 517 | self._folder_remove(folder_name) 518 | self._folder_make(folder_name) 519 | else: 520 | self._folder_make(folder_name) 521 | 522 | self.proc_comm = None 523 | self.proc_size = None 524 | self.proc_rank = None 525 | self.proc_group_comm = None 526 | 527 | for u in self.sg_obj: 528 | if len(u)!=0: 529 | self.sg_obj[u].write(self._folder_sg_path(folder_name,u)) 530 | self.sg_obj[u]=None 531 | 532 | with open(folder_name+'/'+'ddsg', 'wb') as handle: 533 | dill.dump(self, handle, protocol=dill.HIGHEST_PROTOCOL) 534 | 535 | self.proc_comm = MPI.COMM_WORLD.Dup() 536 | self.proc_size = self.proc_comm.Get_size() 537 | self.proc_rank = self.proc_comm.Get_rank() 538 | 539 | for u in self.sg_obj: 540 | if len(u)!=0: 541 | self.sg_obj[u] = Tasmanian.SparseGrid() 542 | self.sg_obj[u].read(self._folder_sg_path(folder_name,u)) 543 | 544 | def _set_group_comm(self,num_tasks:int,verbose:int==0): 545 | """Allocate the MPI processes. 546 | 547 | Args: 548 | num_tasks (int): Number of tasks. 549 | verbose (int, optional): Display runtime information 0,1,2,3,4 and 99. Defaults to 0. 550 | """ 551 | 552 | self.proc_comm.barrier() 553 | 554 | # full model of MPI_COMM 555 | tasks = np.array(range(0,num_tasks)) 556 | proc_ranks = np.array(range(0,self.proc_size)).astype(int) 557 | group_size_floor = max(1,np.floor(self.proc_size/num_tasks)) 558 | group_number = ((proc_ranks / group_size_floor)%num_tasks ).astype(int) 559 | group_sizes = np.bincount(group_number).astype(int) 560 | group_count = len(group_sizes) 561 | 562 | if verbose==99 and self.proc_rank==0: 563 | print('@ MPI Configurations') 564 | print('@ Global Schema:') 565 | print('@ tasks =',tasks) 566 | print('@ proc_ranks =',proc_ranks) 567 | print('@ group_size_floor =',group_size_floor) 568 | print('@ group_number (color) =',group_number) 569 | print('@ group_count =',group_count ) 570 | print('@ group_sizes =',group_sizes) 571 | 572 | 573 | self.proc_group_number = group_number[self.proc_rank] 574 | self.proc_group_comm = self.proc_comm.Split(self.proc_group_number,self.proc_rank) 575 | self.proc_group_rank = self.proc_group_comm.Get_rank() 576 | self.proc_group_size = self.proc_group_comm.Get_size() 577 | self.proc_group_count = group_count 578 | 579 | if verbose==99: 580 | if self.proc_rank==0 : print('@ Local Allocation:') 581 | self.proc_comm.barrier() 582 | time.sleep(self.proc_rank+1) 583 | print('@ rank ',self.proc_rank,'/',self.proc_size,' maps to ','group_rank',self.proc_group_rank,'/',self.proc_group_size) 584 | self.proc_comm.barrier() 585 | 586 | def _folder_sg_path(self,folder_name:str,u:tuple )->str: 587 | """Make folder path for SG object. 588 | 589 | Args: 590 | folder_name (str): Folder name. 591 | u (tuple): Component index of SG. 592 | 593 | Returns: 594 | str: SG folder path. 595 | """ 596 | return folder_name+'/'+','.join(map(str,u))+'.tasmanian' 597 | 598 | def _folder_make(self,folder_name:str): 599 | """Make folder. 600 | 601 | Args: 602 | folder_name (str): Path of the folder. 603 | """ 604 | 605 | self.proc_comm.barrier() 606 | if self.proc_rank==0: 607 | assert not os.path.exists(folder_name), 'The folder exists!' 608 | os.makedirs(folder_name) 609 | 610 | def _folder_remove(self,folder_name:str): 611 | """Remove folder 612 | 613 | Args: 614 | folder_name (str): Path of the folder. 615 | """ 616 | self.proc_comm.barrier() 617 | if self.proc_rank==0: 618 | if os.path.exists(folder_name): 619 | 620 | # delete files in folder 621 | for file_name in os.listdir(folder_name): 622 | file_path = os.path.join(folder_name, file_name) 623 | os.remove(file_path) 624 | 625 | #delete folder 626 | os.rmdir(folder_name) 627 | 628 | def _reset_data_structures(self): 629 | """Rest the datastructures 630 | """ 631 | 632 | self.sg_obj = {} #dic[tuple] ->Tasmanian 633 | 634 | self.hdmr_obj = {} 635 | self.hdmr_obj['eta']={} #dic[string][tuple] ->float 636 | self.hdmr_obj['rho']=[] #dic[string] ->list:float 637 | self.hdmr_obj['cfunc_quad']={} #dic[string][tuple] ->list:float 638 | self.hdmr_obj['quad_k']=[] #dic[string] ->list:float 639 | self.hdmr_obj['vec_coeff']=np.empty(0) #dic[string] ->np.array() 640 | self.hdmr_obj['Z_list']= [] #dic[string] ->list:tuple 641 | 642 | self.num_grid_points = 0 # int 643 | self.num_func = 0 # int 644 | 645 | def _make_sparse_grid(self,l_start:int,l_max:int,sg_tol:float,active_inx:np.array,grid_order:int,grid_rule:str,verbose)->object: 646 | """Wrapper for Tasmanian adaptive SG with MPI prallel function evaluations. 647 | 648 | Args: 649 | l_start (int): Starting refiment level 650 | l_max (int): Maximum refiment level 651 | sg_tol (float): Adaptive SG tolerance. 652 | active_inx (np.array): Indicies for the DDSG component function (zero based index). 653 | grid_order (int): An integer no smaller than -1 indicating the largest polynomial order of the basis functions (see TASMANIAN grid_order). 654 | grid_rule (str): Local polynomial rules (see TASMANIAN grid_rule). 655 | verbose (int): Display runtime information 0,1,2,3,4 and 99. 656 | 657 | Returns: 658 | object: Tasmania SG interpolant. 659 | """ 660 | 661 | assert len(active_inx)>0, 'Active index must at least one active index!' 662 | 663 | # Construct the grid & set the domain 664 | # we start with refinment level l_start 665 | sg = Tasmanian.SparseGrid() 666 | sg.makeLocalPolynomialGrid(len(active_inx),self.m,l_start,grid_order,grid_rule) 667 | 668 | #set domain 669 | grid_domain = self.settings['domain'][active_inx,:] 670 | sg.setDomainTransform(grid_domain) 671 | 672 | # loop through refimnet level upto and including l_max 673 | for l in range(l_start,l_max+1): 674 | 675 | # get grid points and setup for evaluations 676 | grid_points = sg.getNeededPoints() 677 | num_grid_points = sg.getNumNeeded() 678 | 679 | # if no grid points than refinement has ended 680 | if num_grid_points<1: 681 | break 682 | 683 | # mask input in x 684 | if self._flag_no_hdmr : 685 | X_mask= grid_points 686 | else: 687 | X_mask = np.tile(self.x0.flatten(),(num_grid_points,1)) 688 | X_mask[:,active_inx] = grid_points 689 | 690 | if self.proc_group_size == 1 or self.sg_prl==False: 691 | f_val_buffer = self.f_orical(X_mask).reshape(num_grid_points,self.m) 692 | else: 693 | 694 | offset = int(np.ceil(num_grid_points/self.proc_group_size)) 695 | i_begin = max(0,self.proc_group_rank * offset) 696 | i_end = min(num_grid_points,(self.proc_group_rank+1) * offset) 697 | 698 | f_val_buffer_temp = self.f_orical(X_mask[i_begin:i_end,:]) 699 | f_val_buffer_temp = self.proc_group_comm.allgather(f_val_buffer_temp) 700 | 701 | f_val_buffer = np.concatenate(f_val_buffer_temp,axis=0).reshape(num_grid_points,-1) 702 | 703 | # load function values into the SG 704 | sg.loadNeededValues(f_val_buffer) 705 | 706 | # move to the next refinment level 707 | sg.setSurplusRefinement(sg_tol, -1, "classic") 708 | 709 | #if verbose>3 and self.proc_group_rank==0 and self._flag_no_hdmr: print(' ','[MPI Group Size' ,self.proc_group_size, '] SG: l=' ,l_start,'/',l_max,'#grid points=', num_grid_points,' (', np.round(time.time()-t,2),' sec.)') 710 | 711 | return sg 712 | 713 | def _compute_sg_func(self,u:tuple,verbose:bool)->list: 714 | """Generate the DDSG component function. 715 | 716 | Args: 717 | u (tuple): Indicies for the DDSG component function (1 based index). 718 | verbose (int): Display runtime information 0,1,2,3,4 and 99. 719 | 720 | Returns: 721 | list[object,float,float,tuple,tuple]: List of values include, the Tasmania SG interpolant, component function approximate quadrature, active dimension selection eta, component function index, and if rejected index. Note if active dimension selection eta is less than the threshold, then the component function is deemed ignorable, and thus, all values of this list are None, except for the rejected index, which would equal to the component function index. If the component function is not ignorable, only the rejected index is None. 722 | """ 723 | 724 | t = time.time() 725 | 726 | sg_temp = None 727 | cfunc_quad_temp = None 728 | eta_temp = None 729 | u_temp = None 730 | z_temp = None 731 | 732 | l_max = self.settings['l_max'] 733 | l_start = self.settings['l_start'] 734 | eps_sg = self.settings['eps_sg'] 735 | eps_eta = self.settings['eps_eta'] 736 | grid_order = self.settings['grid_order'] 737 | grid_rule = self.settings['grid_rule'] 738 | 739 | # Current index u 740 | active_inx = np.array(u)-1 741 | 742 | # Check if index u should be ignore 743 | candidate_u = True 744 | for z in self.hdmr_obj['Z_list']: 745 | if set(u).issuperset(set(z)): 746 | if verbose>2 and self.proc_group_rank==0: print(' ','[MPI Group Size',self.proc_group_size,'] Index:',u,'[ignored',u,'⊃',z,'] (%0.2e sec.)'%(time.time()-t,)) 747 | candidate_u = False 748 | break 749 | 750 | if candidate_u: 751 | 752 | # Generate Sparse Grid for len(u)-dimensional space 753 | sg = self._make_sparse_grid(l_start,l_max,eps_sg,active_inx,grid_order,grid_rule,verbose) 754 | 755 | q_cfv = 0.0 756 | for r_temp in range(0,len(u)): 757 | V_r = list(combinations(u,r_temp)) 758 | for v in V_r: 759 | if v in list(self.sg_obj.keys()): 760 | q_cfv += self.hdmr_obj['cfunc_quad'][v] 761 | 762 | # sample the space 763 | # We can take the sg quadrature but ... simple sampling is enough 764 | sg_mc_quad = np.mean((sg.evaluateBatch(self.X_sample[:,active_inx])))*np.product(self.settings['domain'][:,1]-self.settings['domain'][:,0]) 765 | q_cfu =sg_mc_quad - q_cfv 766 | 767 | quad_norm_k_minus_1 = np.linalg.norm(self.hdmr_obj['quad_k'][-1] ) 768 | quad_nrom_cfu = np.linalg.norm(q_cfu) 769 | 770 | # small number self.zero to previent numerical issues 771 | eta = (quad_nrom_cfu) / (self.zero+quad_norm_k_minus_1) 772 | 773 | if eta > eps_eta: 774 | sg_temp = sg 775 | cfunc_quad_temp = q_cfu 776 | eta_temp = eta 777 | u_temp = u 778 | z_temp = None 779 | if verbose>2 and self.proc_group_rank==0: print(' ','[MPI Group Size',self.proc_group_size,'] u=%s accepted η=%0.2e>%0.2e (%0.2e sec.)'%(u,eta,eps_eta,time.time()-t)) 780 | else: 781 | sg_temp = None 782 | cfunc_quad_temp = None 783 | eta_temp = None 784 | u_temp = None 785 | z_temp = u 786 | if verbose>2 and self.proc_group_rank==0: print(' ','[MPI Group Size',self.proc_group_size,'] u=%s ignored η=%0.2e≤%0.3e (%0.2e sec.)'%(u,eta,eps_eta,time.time()-t)) 787 | 788 | return [sg_temp,cfunc_quad_temp,eta_temp,u_temp,z_temp] 789 | -------------------------------------------------------------------------------- /DDSG/__init__.py: -------------------------------------------------------------------------------- 1 | from .DDSG import DDSG 2 | -------------------------------------------------------------------------------- /IRBC/IRBC.py: -------------------------------------------------------------------------------- 1 | """ 2 | This Python code accompanies the review article by Brumm, Krause, Schaab, & Scheidegger (2021) 3 | and corresponds to the International Real Business Cycle (IRBC) model. See paper for further details. 4 | 5 | This class is a reimplentation of the model outlined in https://github.com/SparseGridsForDynamicEcon/SparseGrids_in_econ_handbook 6 | 7 | """ 8 | 9 | 10 | import numpy as np 11 | from tabulate import tabulate 12 | 13 | class IRBC: 14 | 15 | def __init__(self, num_countries:int, irbc_type:str): 16 | """Constructor of IRBC class 17 | 18 | Args: 19 | num_countries (int): Number of countries in the model. 20 | irbc_type (str): Type of IRBC model (smooth or non-smooth) 21 | """ 22 | 23 | ## Economic parameters 24 | #assert irbc_type=='smooth' 25 | 26 | # Intertemporal elasticity of substitution 27 | self.gamma = None 28 | self.ies_a = None 29 | self.ies_b = None 30 | 31 | # Discount factor 32 | self.beta = None 33 | # Capital share of income 34 | self.zeta = None 35 | # Depreciation rate 36 | self.delta = None 37 | # Persistence of TFP shocks 38 | self.rho_Z = None 39 | # Standard deviation of TFP shocks 40 | self.sig_E = None 41 | # Intensity of capital adjustment costs 42 | self.kappa = None 43 | # Aggregate productivity 44 | self.A_tfp = None 45 | # Welfare weight 46 | self.pareto = None 47 | # Lower bound for capital 48 | self.k_min = None 49 | # Upper bound for capital 50 | self.k_max = None 51 | 52 | # Lower bound for TFP 53 | self.a_min = None 54 | # Upper bound for TFP 55 | self.a_max = None 56 | 57 | # Number of countries 58 | self.num_countries = num_countries 59 | 60 | # Number of shocks (Country-specific shocks + aggregate shock) 61 | self.num_shocks = self.num_countries+1 62 | 63 | # Number of policies (nCountries+1 for smooth IRBC, nCountries*2+1 for nonsmooth) 64 | self.irbc_type=irbc_type 65 | if self.irbc_type=='non-smooth': 66 | self.num_policies = self.num_countries*2+1 67 | 68 | 69 | elif self.irbc_type=='smooth': 70 | self.num_policies = self.num_countries+1 71 | 72 | self.grid_dim = self.num_countries*2 73 | self.grid_dof = self.num_policies 74 | 75 | self.flag_set_param = False 76 | self.flag_set_integral = False 77 | 78 | def set_parameters(self,ies_a:float=0.25,ies_b:float=1,beta:float=0.99,zeta:float=0.36,delta:float=0.01,rho_Z:float=0.95,sig_E:float=0.01,kappa:float=0.5,k_min:float=0.8,k_max:float=1.2): 79 | """Set IRBC paramters. Note the defualt settings follows the model described in https://github.com/SparseGridsForDynamicEcon/SparseGrids_in_econ_handbook. 80 | 81 | Args: 82 | ies_a (float, optional): Intertemporal elasticity of substitution upper. Defaults to 0.25. 83 | ies_b (float, optional): Intertemporal elasticity of substitution lower. Defaults to 1. 84 | beta (float, optional): Discount factor. Defaults to 0.99. 85 | zeta (float, optional): Capital share of income. Defaults to 0.36. 86 | delta (float, optional): Depreciation rate. Defaults to 0.01. 87 | rho_Z (float, optional): Standard deviation of TFP shocks. Defaults to 0.95. 88 | sig_E (float, optional): Standard deviation of TFP shocks. Defaults to 0.01. 89 | kappa (float, optional): Intensity of capital adjustment costs. Defaults to 0.5. 90 | k_min (float, optional): Lower bound for capital. Defaults to 0.8. 91 | k_max (float, optional): Upper bound for capital. Defaults to 1.2. 92 | """ 93 | 94 | # Intertemporal elasticity of substitution 95 | self.ies_a = ies_a 96 | self.ies_b = ies_b 97 | self.gamma = np.zeros(self.num_countries) 98 | for i in range(0,self.num_countries): 99 | self.gamma[i] = ies_a+i*(ies_b-ies_a)/(self.num_countries-1) 100 | 101 | # Discount factor 102 | self.beta = beta 103 | # Capital share of income 104 | self.zeta = zeta 105 | # Depreciation rate 106 | self.delta = delta 107 | # Persistence of TFP shocks 108 | self.rho_Z = rho_Z 109 | # Standard deviation of TFP shocks 110 | self.sig_E = sig_E 111 | # Intensity of capital adjustment costs 112 | self.kappa = kappa 113 | 114 | # Lower bound for capital 115 | self.k_min = k_min 116 | # Upper bound for capital 117 | self.k_max = k_max 118 | 119 | # Aggregate productivity 120 | self.A_tfp = (1.0-self.beta*(1.0-self.delta))/(self.zeta*self.beta) 121 | # Welfare weight 122 | self.pareto = self.A_tfp**(1.0/self.gamma) 123 | 124 | # Lower bound for TFP 125 | self.a_min = -0.8*self.sig_E/(1.0-self.rho_Z) 126 | # Upper bound for TFP 127 | self.a_max = 0.8*self.sig_E/(1.0-self.rho_Z) 128 | 129 | # set flag 130 | self.flag_set_param = True 131 | 132 | def set_integral_rule(self,quadrature_type:str='monomials_power'): 133 | """Set numerical itegration/quadrature rule. This is fixed to 'monomials_power' until further development. 134 | 135 | Args: 136 | quadrature_type (str, optional): Type of quadrature rule. Defaults to 'monomials_power'. 137 | """ 138 | 139 | #assert quadrature_type=='GH-quadrature' or quadrature_type=='monomials_2d' or quadrature_type=='monomials_power' 140 | assert quadrature_type=='monomials_power' 141 | assert self.flag_set_param==True 142 | 143 | # Type of quadrature values 144 | self.quadrature_type = quadrature_type 145 | 146 | # Number of integration nodes 147 | self.num_integral_nodes = 2*self.num_shocks**2 + 1 148 | 149 | # Deviations in one dimension (note that the origin is row zero) 150 | z0 = np.zeros((self.num_integral_nodes,self.num_shocks)) 151 | for i1 in range(self.num_shocks): 152 | z0[i1*2+1,i1] = 1.0 153 | z0[i1*2+2,i1] = -1.0 154 | 155 | i0 = 0 156 | # Deviations in two dimensions 157 | for i1 in range(self.num_shocks): 158 | for i2 in range(i1+1,self.num_shocks): 159 | z0[2*self.num_shocks+1+i0*4,i1] = 1.0 160 | z0[2*self.num_shocks+2+i0*4,i1] = 1.0 161 | z0[2*self.num_shocks+3+i0*4,i1] = -1.0 162 | z0[2*self.num_shocks+4+i0*4,i1] = -1.0 163 | z0[2*self.num_shocks+1+i0*4,i2] = 1.0 164 | z0[2*self.num_shocks+2+i0*4,i2] = -1.0 165 | z0[2*self.num_shocks+3+i0*4,i2] = 1.0 166 | z0[2*self.num_shocks+4+i0*4,i2] = -1.0 167 | i0 += 1 168 | 169 | # Nodes 170 | integral_nodes = np.zeros((self.num_integral_nodes,self.num_shocks)) 171 | integral_nodes[1:self.num_shocks*2+1,:] = z0[1:self.num_shocks*2+1,:]*np.sqrt(2.0+self.num_shocks)*self.sig_E 172 | integral_nodes[self.num_shocks*2+1:] = z0[self.num_shocks*2+1:]*np.sqrt((2.0+self.num_shocks)/2.0)*self.sig_E 173 | 174 | # Weights 175 | integral_weights = np.zeros((self.num_integral_nodes)) 176 | 177 | integral_weights[0] = 2.0/(2.0+self.num_shocks) 178 | integral_weights[1:self.num_shocks*2+1] = (4-self.num_shocks)/(2*(2+self.num_shocks)**2) 179 | integral_weights[self.num_shocks*2+1:] = 1.0/(self.num_shocks+2)**2 180 | 181 | self.integral_nodes = integral_nodes 182 | self.integral_weights = integral_weights 183 | self.flag_set_integral = True 184 | 185 | def system_of_equations(self,x:np.array,state:np.array,grid:object)->np.array: 186 | """AI is creating summary for system_of_equations 187 | 188 | Args: 189 | x (np.array): [description] 190 | state (np.array): The values of the state variables 191 | grid (object): The policiy function interpolant 192 | 193 | Returns: 194 | np.array: [description] 195 | """ 196 | 197 | # State variables 198 | capStates = state[0:self.num_countries] 199 | tfpStates = state[self.num_countries:] 200 | 201 | # Policy values 202 | capPolicies = x[0:self.num_countries] 203 | lamb = x[self.num_countries] 204 | 205 | if self.irbc_type == 'non-smooth': 206 | gz_alphas = x[self.num_countries+1:] 207 | 208 | # Garcia-Zengwill transformation of the occasionally binding constraints 209 | gz_alpha_plus = np.maximum(0.0, gz_alphas) 210 | gz_alpha_minus = np.maximum(0.0,-gz_alphas) 211 | 212 | # Computation of integrands 213 | Integrands = self.expectation_of_FOC(capPolicies, state, grid) 214 | 215 | IntResult = np.empty(self.num_countries) 216 | 217 | for i in range(self.num_countries): 218 | IntResult[i] = np.dot(self.integral_weights,Integrands[:,i]) 219 | 220 | res = np.zeros(self.num_policies) 221 | 222 | # Computation of residuals of the equilibrium system of equations 223 | 224 | if self.irbc_type=='non-smooth': 225 | # Euler equations & GZ alphas 226 | for ires in range(0,self.num_countries): 227 | res[ires] = (self.beta*IntResult[ires] + gz_alpha_plus[ires])\ 228 | /(1.0 + self.AdjCost_ktom(capStates[ires],capPolicies[ires])) - lamb 229 | res[self.num_countries+1+ires] = capPolicies[ires] - capStates[ires]*(1.0-self.delta) - gz_alpha_minus[ires] 230 | else: 231 | # Euler equations 232 | for ires in range(0,self.num_countries): 233 | res[ires] = self.beta*IntResult[ires]/(1.0 + self.AdjCost_ktom(capStates[ires],capPolicies[ires])) - lamb 234 | 235 | 236 | # Aggregate resource constraint 237 | for ires2 in range(0,self.num_countries): 238 | res[self.num_countries] += self.F(capStates[ires2],tfpStates[ires2]) + (1.0-self.delta)*capStates[ires2] - capPolicies[ires2]\ 239 | - self.AdjCost(capStates[ires2],capPolicies[ires2])\ 240 | - (lamb/self.pareto[ires2])**(-1.0/self.gamma[ires2]) 241 | 242 | 243 | return res 244 | 245 | def expectation_of_FOC(self,ktemp:np.array, state:np.array, grid:object)->np.array: 246 | """Compute the expectation of the terms in the Euler equations of each country. 247 | 248 | Args: 249 | ktemp (np.array): The values for the capital policies 250 | state (np.array): The values of the state variables 251 | grid (object): Interpolant of the policy function 252 | 253 | Returns: 254 | np.array: The expectation terms for each country in each possible state tomorrow 255 | """ 256 | 257 | # 1) Determine next period's tfp states 258 | 259 | new_state = np.zeros((self.num_integral_nodes,self.num_countries)) 260 | 261 | for itfp in range(self.num_countries): 262 | new_state[:,itfp] = self.rho_Z*state[self.num_countries+itfp] + (self.integral_nodes[:,itfp] + self.integral_nodes[:,self.num_shocks-1]) 263 | new_state[:,itfp] = np.where(new_state[:,itfp] > self.a_min, new_state[:,itfp], self.a_min) 264 | new_state[:,itfp] = np.where(new_state[:,itfp] < self.a_max, new_state[:,itfp], self.a_max) 265 | 266 | # 2) Determine next period's state variables 267 | evalPt = np.zeros((self.num_integral_nodes,self.num_countries*2)) 268 | evalPt[:,0:self.num_countries] = ktemp 269 | evalPt[:,self.num_countries:] = new_state 270 | 271 | # 3) Determine relevant variables within the expectations operator 272 | fval = grid.eval(evalPt) 273 | capPrPr = fval[:,0:self.num_countries] 274 | lambPr = fval[:,self.num_countries] 275 | #capPrPr = grid.evaluateBatch(evalPt)[:,0:self.num_countries] 276 | #lambPr = grid.evaluateBatch(evalPt)[:,self.num_countries] 277 | 278 | if self.irbc_type=='non-smooth': 279 | #gzAlphaPr = grid.evaluateBatch(evalPt)[:,self.num_countries+1:] 280 | gzAlphaPr = fval[:,self.num_countries+1:] 281 | gzAplusPr = np.maximum(0.0,gzAlphaPr) 282 | 283 | # Compute tomorrow's marginal productivity of capital 284 | MPKtom = np.zeros((self.num_integral_nodes,self.num_countries)) 285 | for impk in range(self.num_countries): 286 | MPKtom[:,impk] = 1.0 - self.delta + self.Fk(ktemp[impk],new_state[:,impk]) - self.AdjCost_k(ktemp[impk],capPrPr[:,impk]) 287 | 288 | 289 | density = 1.0 290 | 291 | #Specify Integrand 292 | val = np.zeros((self.num_integral_nodes,self.num_countries)) 293 | 294 | if self.irbc_type=='non-smooth': 295 | for iexp in range(self.num_countries): 296 | val[:,iexp] = (MPKtom[:,iexp]*lambPr - (1.0-self.delta)*gzAplusPr[:,iexp]) * density 297 | 298 | else: 299 | for iexp in range(self.num_countries): 300 | val[:,iexp] = MPKtom[:,iexp]*lambPr * density 301 | 302 | 303 | return val 304 | 305 | def print_parameters(self): 306 | """Print the parameters set by set_parameters. 307 | """ 308 | 309 | H=['Parameter','Variable','Value'] 310 | T=[] 311 | T.append(['Intertemporal elasticity of substitution(IES)','gamma','ies_a+(i-1)(ies_b-ies_a)/(N-1)']) 312 | T.append(['IES factor a','ies_a',self.ies_a]) 313 | T.append(['IES factor b','ies_b',self.ies_b]) 314 | T.append(['Discount factor','beta',self.beta]) 315 | T.append(['Capital share of income','zeta',self.zeta]) 316 | T.append(['Depreciation rate','delta',self.delta]) 317 | T.append(['Persistence of total factor productivity shocks','rho_Z',self.rho_Z]) 318 | T.append(['Standard deviation of total factor productivity shocks','sig_E',self.sig_E]) 319 | T.append(['Intensity of capital adjustment costs','kappa',self.kappa]) 320 | T.append(['Lower bound for capital','k_min',self.k_min]) 321 | T.append(['Upper bound for capital','k_max',self.k_max]) 322 | T.append(['Aggregate productivity','A_tfp',self.A_tfp]) 323 | T.append(['Welfare weight','pareto',self.pareto]) 324 | T.append(['Lower bound for total factor productivity','a_min',self.a_min]) 325 | T.append(['Upper bound for total factor productivity','a_max',self.a_max]) 326 | 327 | print(tabulate(T,headers=H)) 328 | 329 | def error_sim(self,policy_funcion:object,N:int)->np.array: 330 | """Compute the error measures along the simulation path of the given policy function. 331 | 332 | 333 | Args: 334 | policy_funcion (object): Policy function. 335 | N (int): Number of steps in the simulation. 336 | 337 | Returns: 338 | np.array: [description] 339 | """ 340 | 341 | state_current = np.zeros(shape=(1,self.grid_dim)) 342 | state_current[0,0:self.num_countries] = (self.k_min + self.k_max)/2 343 | state_current[0,self.num_countries:2*self.num_countries+1] = (self.a_min + self.a_max)/2 344 | 345 | error = np.zeros(shape=(N,self.num_countries)) 346 | 347 | for t in range(0,N): 348 | shock_local = np.random.normal(0,1,(self.num_countries)) 349 | shock_global = np.random.normal(0,1,(1)) 350 | 351 | captial_current = state_current[0,0:self.num_countries] 352 | productivity_current = state_current[0,self.num_countries:2*self.num_countries] 353 | policy_current = policy_funcion.eval(state_current) 354 | 355 | capital_next = policy_current[0,0:self.num_countries] 356 | lambda_next = policy_current[0,self.num_countries:self.num_countries+1] 357 | productivity_next = self.rho_Z*productivity_current+ self.sig_E*(shock_local+shock_global) 358 | mu_current = np.zeros(self.num_countries) 359 | if self.irbc_type=='non-smooth': 360 | mu_current = policy_current[0,self.num_countries+1:2*self.num_countries+1] 361 | 362 | state_next = np.concatenate([capital_next,productivity_next]) 363 | 364 | #Compute Density 365 | density = 1.0 366 | 367 | error_ee = np.empty(self.num_countries) 368 | error_ic = np.empty(self.num_countries) 369 | 370 | for i in range(0,self.num_integral_nodes): 371 | 372 | # E[ln a_{i,t}] = E[\rho ln a_{i,t-1} + \sigma (e_{i,t} + e_{N,t})] 373 | # note e_{N,t} is the global shock fixed to the last country 374 | productivity_next_expectation = self.rho_Z*productivity_next + (self.integral_nodes[i,0:self.num_countries] + self.integral_nodes[i,-1]) 375 | state_next_expectation = np.concatenate([capital_next,productivity_next_expectation]).reshape((1,-1)) 376 | 377 | policy_next_expectation = policy_funcion.eval(state_next_expectation) 378 | capital_next_expectation = policy_next_expectation[0,0:self.num_countries] 379 | lambda_next_expectation = policy_next_expectation[0,self.num_countries] 380 | 381 | marginal_cost_of_captial_next = 1.0 - self.delta + self.Fk(capital_next,productivity_next_expectation) - self.AdjCost_k(capital_next,capital_next_expectation) 382 | 383 | if self.irbc_type=='non-smooth': 384 | mu_next_expectation = policy_next_expectation[0,self.num_countries+1:2*self.num_countries+1] 385 | temp = (lambda_next_expectation*marginal_cost_of_captial_next - (1.0-self.delta)*mu_next_expectation) * density 386 | else: 387 | temp = lambda_next_expectation*marginal_cost_of_captial_next * density 388 | 389 | error_ee = error_ee + temp*self.integral_weights[i] 390 | 391 | error_ee = self.beta*error_ee/(lambda_next*(1.0+self.AdjCost_ktom(captial_current,capital_next))) - 1.0 392 | error_ic = 1.0 - captial_current/(capital_next*(1.0-self.delta)) 393 | 394 | if self.irbc_type=='non-smooth': 395 | for i in range(0,self.num_countries): 396 | error[t,i]= max(error_ee[i],error_ic[i],np.minimum(-error_ee[i],-error_ic[i])) 397 | else: 398 | error[t,:] = error_ee 399 | 400 | #update current state 401 | state_current = state_next.reshape((1,-1)) 402 | 403 | return error 404 | 405 | 406 | def F(self,capital:np.array,sh:np.array)->np.array: 407 | """ Production function 408 | 409 | Args: 410 | capital (np.array): Capital 411 | sh (np.array): Productivity shock 412 | 413 | Returns: 414 | np.array: Production 415 | """ 416 | 417 | val = self.A_tfp * np.exp(sh)*np.maximum(capital,1e-6)**self.zeta 418 | 419 | return val 420 | 421 | def Fk(self,capital:np.array,sh:np.array)->np.array: 422 | """Marginal product of capital 423 | 424 | Args: 425 | capital (np.array): Capital 426 | sh (np.array): Productivity shock 427 | 428 | Returns: 429 | np.array: Marginal product of capital 430 | """ 431 | val = self.A_tfp * self.zeta*np.exp(sh)*np.maximum(capital,1e-6)**(self.zeta-1.0) 432 | 433 | return val 434 | 435 | def AdjCost(self,ktod:np.array,ktom:np.array)->np.array: 436 | """Capital adjustment cost 437 | 438 | Args: 439 | ktod (np.array): Captial today 440 | ktom (np.array): Captial tommorow 441 | 442 | Returns: 443 | np.array: Capital adjustment cost 444 | """ 445 | 446 | captod = np.maximum(ktod,1e-6) 447 | captom = np.maximum(ktom,1e-6) 448 | 449 | j = captom/captod - 1.0 450 | val = 0.5 * self.kappa * j * j * captod 451 | 452 | return val 453 | 454 | def AdjCost_k(self,ktod:np.array,ktom:np.array)->np.array: 455 | """Derivative of capital adjustment cost w.r.t today's cap stock 456 | 457 | Args: 458 | ktod (np.array): Captial today 459 | ktom (np.array): Captial tommorow 460 | 461 | Returns: 462 | np.array: Derivative of capital adjustment cost w.r.t today's cap stock 463 | """ 464 | 465 | captod = np.maximum(ktod,1e-6) 466 | captom = np.maximum(ktom,1e-6) 467 | 468 | j = captom/captod - 1.0 469 | j1 = captom/captod + 1.0 470 | val = (-0.5)*self.kappa*j*j1 471 | 472 | return val 473 | 474 | def AdjCost_ktom(self,ktod:np.array,ktom:np.array)->np.array: 475 | """Derivative of capital adjustment cost w.r.t tomorrows's cap stock 476 | 477 | Args: 478 | ktod (np.array): Captial today 479 | ktom (np.array): Captial tommorow 480 | 481 | Returns: 482 | np.array: Derivative of capital adjustment cost w.r.t tomorrows's cap stock 483 | """ 484 | 485 | captod = np.maximum(ktod,1e-6) 486 | captom = np.maximum(ktom,1e-6) 487 | 488 | j = captom/captod - 1.0 489 | val = self.kappa * j 490 | 491 | 492 | return val 493 | 494 | def ARC_zero(self,lam_gues,gridPt)->float: 495 | """ Residual of aggregate resource constraint, used compute an initial guess for the ARC multiplier. 496 | 497 | Args: 498 | lam_gues ([type]): [description] 499 | gridPt ([type]): [description] 500 | 501 | Returns: 502 | float: [description] 503 | """ 504 | 505 | res = 0.0 506 | 507 | for i1 in range(self.num_countries): 508 | res += np.exp(gridPt[self.num_countries+i1])*self.A_tfp*gridPt[i1]**self.zeta - (-self.delta*self.kappa/2.0)**2 - (lam_gues/self.pareto[i1])**(-self.gamma[i1]) 509 | 510 | return res 511 | 512 | 513 | -------------------------------------------------------------------------------- /IRBC/__init__.py: -------------------------------------------------------------------------------- 1 | from .IRBC import IRBC 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Aryan Eftekhari, Simon Scheidegger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # High-Dimensional Dynamic Stochastic Model Representation 2 | This code repository supplements the work of Eftekhari and Scheidegger, titled _[High-Dimensional Dynamic Stochastic Model Representation](#publication)_ (Eftekhari and Scheidegger; SIAM SISC 2022), which introduces a highly scalable function approximation technique using Dimensional Decomposition and adaptive Sparse Grid (DDSG) to solve dynamic stochastic economic models. The DDSG algorithm is embedded in a time-iteration algorithm to solve high-dimensional, nonlinear dynamic stochastic economic models. Furthermore, the introduced method can trivilly be extended to solve models with value function iteration. Note that our algorithm was originally developed in C++ and Fortran using hybrid parallelism (OpenMP and MPI); however, the MPI parallel Python implementation presented here is intended to be more practical, while still being decently performant in. Concretely: 3 | 4 | * This repository provides a versatile and generic method for approximating very high-dimensional functions. 5 | * This repository provides a method that is applicable in computing recursive equilibria of nonlinear dynamic stochastic economic models with many state variables. 6 | * The method has been demonstrated in the accompanied article that dynamic stochastic models with up to 300 state variables could be solved globally using DDSG. 7 | * This repository aims to make our method easily accessible to the computational economics and finance community. 8 | 9 | ![image](https://drive.google.com/uc?id=120KCXRvqwZHefPsUbjmkLex8pK5PMf1S) 10 | 11 | This figure is a visual representation of one step of the time-iteration algorithm. We solve the first-order conditions (FOC) of the model for the state variable 12 | $x_t$ 13 | in the updated policy function 14 | $\tilde{p}'$ 15 | (left), using the policy function from the previous time iteration step 16 | $\tilde{p}$ 17 | (right). In the following description, 18 | $\tilde{p}'$ 19 | and 20 | $\tilde{p}$ 21 | correspond to `p_next` and `p_last`, respectivily. These policy functions are approximated using DDSG. 22 | 23 | ## Libraries 24 | The primary libraries introduced in this repository are _DDSG_ for function approximation (used for both DDSG and adaptive SG) and _IRBC_ for IRBC model description. 25 | 26 | ### lib/DDSG.py 27 | The DDSG technique is a grid-based function approximation method that combines High-Dimension Model Representation, a variant of _Dimensional Decomposition_ (DD), and adaptive _Sparse Grids_ (SG). The combined approach enables a highly performant and scalable gird base function approximation method that can scale efficiently to high dimensions and utilize distributed memory architectures. This library is user-friendly and parallelized with MPI. The SG components of the algorithm use the [Tasmanian](https://tasmanian.ornl.gov) open-source SG library. Instructions on how to install DDSG can be found [here](#Installation). 28 | 29 | #### Usage 30 | Using the DDSG requires the following steps: 31 | 1. Instantiate DDSG with the function to be approximated and the dimension of the domain, e.g., `ddsg=DDSG()` and `ddsg.init(f,d)`. If you are loading the object from a file, we can call `ddsg=DDSG(path_to/my_ddsg_obj)`. 32 | 2. Set the parameters for the adaptive SG (see documentation for details), e.g., `ddsg.set_grid(l_max=10,eps_sg=1e-6)`. 33 | 3. Set the parameters for the DD decomposition (see documentation for details), e.g., `ddsg.set_decomposition(x0=np.ones(d)/2,k_max=1)`. If `set_decomposition()` is not invoked, the DDSG class works as a wrapper for the Tasmanian library, with built-in MPI parallelism. 34 | 4. Build the approximation, e.g., `ddsg.build(verbose=1)`. 35 | 36 | If a specific grid point evaluation is computationally demanding, we can use MPI processes to evaluate the needed grid points in parallel by setting `ddsg.sg_prl=True` (by default `ddsg.sg_prl=False`). If the number of available MPI processes exceeds the number of component functions, the extra processes are assigned in the SG computation if `ddsg.sg_prl=True`. 37 | 38 | ``` 39 | #Example ddsg usage 40 | 41 | from DDSG import DDSG 42 | import numpy as np 43 | 44 | d=10 45 | f_example = lambda X: -15*np.sum( np.abs(X-4/11) ,axis=1) 46 | x0=np.ones(shape=(1,d))/2 47 | 48 | domain =np.zeros(shape=(d,2)) 49 | domain[:,1]=1 50 | 51 | ddsg=DDSG() 52 | ddsg.init(f_orical=f_example, d=d) 53 | ddsg.set_grid(domain=domain,l_max=4,eps_sg=1e-6) 54 | ddsg.set_decomposition(x0=x0,k_max=1) 55 | ddsg.build(verbose=1) 56 | 57 | x=np.random.uniform(low=0, high=1,size=x0.shape) 58 | print('val_est=',ddsg.eval(x)) 59 | print('val_true=',f_example(x)) 60 | ``` 61 | 62 | #### Parallel Execuation 63 | Parallel execution follows straightforwardly using `mpirun`. It is recommended that `--bind-to core` option be used to ensure that the MPI processes are bound to physical cores. 64 | ``` 65 | mpirun -np 4 --bind-to core python3 file_to_run.py 66 | ``` 67 | _Note that for parallelization within the sparse grid, the option `ddsg.sg_prl=True` must be set._ 68 | 69 | 70 | ### lib/IRBC.py 71 | The International Real Business Cycle (IRBC) library supports two models: _smooth_ and _non-smooth_. We refer to an IRBC model as smooth if there are no kinks in the policies and non-smooth if there are non-differentiabilities in the latter functions. The models are simple to describe, have a unique solution, and their dimensionality can be meaningfully scaled up. As such, these models are used to test various solution strategies for large-scale dynamic stochastic economic models. This model trait allows us to focus on the computational problems of dealing with high-dimensional state spaces. 72 | 73 | _The models are implemented and parameterized (by default) as per the article by Brumm and Scheidegger titled [Using Adaptive Sparse Grids to Solve High-Dimensional Dynamic Models]([https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3979412](https://onlinelibrary.wiley.com/doi/abs/10.3982/ECTA12216))(2017). For further details, we refer the reader to that article._ 74 | 75 | #### Usage 76 | Using the IRBC model requires the following steps: 77 | 1. Instatitate the IRBC model with the number of countries (say 2) and type of model (smooth or non-smooth), i.e., `model=IRBC(num_countries=2,irbc_type='non-smooth')`. 78 | 2. Set the parameters of the model, i.e.,`.set_parameters()`. Note that all parameters are set by default to those found in the aforementioned publications. 79 | 3. Set the default integral rules, i.e., `.set_integral_rule()`. Note that at the moment, only 'monomials_power' has been implemented, and this is selected by default. 80 | 4. (optional) For confirmation, we can print the parameters of the model using `print_parameters()` method. 81 | 82 | ``` 83 | from IRBC import IRBC 84 | import numpy as np 85 | 86 | model = IRBC(num_countries=2, irbc_type='non-smooth') 87 | model.set_parameters() 88 | model.set_integral_rule() 89 | model.print_parameters() 90 | ``` 91 | 92 | #### Integration with DDSG 93 | The IRBC library is intended to be used with the DDSG library. The main method of the IRBC object is the `.system_of_equations(x, state,p_last)`, which is the value of the residual of the first-order-conditions optimality conditions of the model. In particular, `x` is the policy to be solved at the state denoted by `state`, and `grid` is the last best estimate of the policy function (i.e., a DDSG approximation of the current policy function). The zeros of this system of (non-linear) equations must be solved for all discrete states (i.e., grid points) in the state space. 94 | 95 | The following example shows how we can incorporate the DDSG into the IRBC mode. In this case, `p_rand` is a function with appropriate bounds (all set to that of the capital range k_min to k_max). From here on, we generate a DDSG approximation of the random function, which we treat as the estimate of some policy function called `p_last`. Finally, we can compute the residual of first-order optimality conditions if the optimal policy `x=p_guess` at state `state=state` is given the current DDSG policy approximation `grid=p_rand`. 96 | 97 | ``` 98 | from DDSG import DDSG 99 | from IRBC import IRBC 100 | import numpy as np 101 | 102 | model = IRBC(num_countries=2, irbc_type='smooth') 103 | model.set_parameters() 104 | model.set_integral_rule() 105 | 106 | p_rand = lambda X: np.random.uniform(low=model.k_min, high=model.k_max, size=(X.shape[0],model.grid_dof)) 107 | 108 | domain = np.zeros((model.grid_dim,2)) 109 | domain[0:model.num_countries,0] = model.k_min 110 | domain[0:model.num_countries,1] = model.k_max 111 | domain[model.num_countries:,0] = model.a_min 112 | domain[model.num_countries:,1] = model.a_max 113 | 114 | x0 = np.mean(domain,axis=1).reshape((1,domain.shape[0])) 115 | 116 | p_last = DDSG() 117 | p_last.init(f_orical=p_rand,d=model.grid_dim,m=model.grid_dof) 118 | p_last.set_grid(domain=domain,l_max=4,eps_sg=1e-3) 119 | p_last.set_decomposition(x0,k_max=1,eps_rho=1e-3,eps_eta=1e-3) 120 | p_last.build(verbose=0) 121 | 122 | state = np.array([1.0,1.0,0.0,0.0]) 123 | p_guess = np.array([1.0,1.0,1.0,-0.1]) 124 | foc_residual = model.system_of_equations(x=p_guess,state=state,grid=p_rand) 125 | print('foc_residual=',foc_residual) 126 | 127 | ``` 128 | 129 | ## Examples: 130 | The first example provided outlines analytical test cases for the general DDSG function approximation technique, whereas the second example focuses on using DDSG as part of the IRBC model solution. Both examples include a tutorial base Jupiter notebook that is intended to be pedagogical. Furthermore, we provided standalone Python scripts for each of the examples to highlight the performance and scalability of the introduced computational methods. 131 | 132 | ### examples/analytical 133 | The analytical examples provided for DDSG cover the following topics: 134 | 1. How to use the DDSG library. 135 | 2. Fundamentals of SG and DDSG approximation. 136 | 3. Seperablity of functions (i.e., decomposition). 137 | 4. Performance and the effect of cure-of-dimensionality. 138 | 5. Scalability and execution on distributed memory architectures (parallel execution). _The standalone python script `examples/analytical/unit_test.py` is used in these tests._ 139 | 140 | [![Generic badge](https://img.shields.io/badge/jupyter%20nbviewer-DDSG-green)](https://nbviewer.org/github/SparseGridsForDynamicEcon/HDMR/blob/main/examples/analytical/tutorial.ipynb) 141 | 142 | 143 | ### examples/irbc 144 | The IRBC examples provided here cover the following topics: 145 | 1. How to use the IRBC library. 146 | 2. Incorporating the DDSG library within the IRBC mode. 147 | 3. Using the time-iteration method (along with the DDSG library) to solve the optimal policy of the IRBC mode. 148 | 4. Using the DDSG library to run both SG and DDSG approximation of the optimal policy function. 149 | 5. Computing metrics such as stagnation and simulation error of the policy function. 150 | 6. Scalability and performance of using DDSG in place of just SG (parallel execution). _The standalone python script `examples/irbc/unit_test.py` is used in these tests._ 151 | 152 | [![Generic badge](https://img.shields.io/badge/jupyter%20nbviewer-IRBC-green)](https://nbviewer.org/github/SparseGridsForDynamicEcon/HDMR/blob/main/examples/irbc/tutorial.ipynb) 153 | 154 | 155 | ## Publication 156 | 157 | Please cite [High-Dimensional Dynamic Stochastic Model Representation, A. Eftekhari, S. Scheidegger, SIAM Journal on Scientific Computing (SISC), 2022](https://epubs.siam.org/doi/10.1137/21M1392231) in your publications if it helps your research: 158 | ``` 159 | @article{doi:10.1137/21M1392231, 160 | author = {Eftekhari, Aryan and Scheidegger, Simon}, 161 | title = {High-Dimensional Dynamic Stochastic Model Representation}, 162 | journal = {SIAM Journal on Scientific Computing}, 163 | volume = {44}, 164 | number = {3}, 165 | pages = {C210-C236}, 166 | year = {2022}, 167 | doi = {10.1137/21M1392231} 168 | } 169 | ``` 170 | See [here](https://arxiv.org/pdf/2202.06555.pdf) for an archived version of the article. 171 | 172 | 173 | ### Authors 174 | * [Aryan Eftekhari](https://scholar.google.com/citations?user=GiugKBsAAAAJ&hl=en) (Department of Economics, University of Lausanne) 175 | * [Simon Scheidegger](https://sites.google.com/site/simonscheidegger/) (Department of Economics, University of Lausanne) 176 | 177 | 178 | ### Other Relate Resreach 179 | * [Using Adaptive Sparse Grids to Solve High-Dimensional Dynamic Models; Brumm & Scheidegger (2017)](https://onlinelibrary.wiley.com/doi/abs/10.3982/ECTA12216)). 180 | * [Sparse Grids for Dynamic Economic Models; Brumm et al. (2022)](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3979412) 181 | 182 | 183 | ## Installation 184 | 185 | ### Quick start installation of prerequisites 186 | ```shell 187 | $ pip3 install -r requirements.txt 188 | ``` 189 | 190 | ### Detailed installation of prerequisites 191 | _SG library (required by DDSG)_ 192 | ```shell 193 | $ pip3 install Tasmanian 194 | ``` 195 | For further information on alternative installation procedures, see https://tasmanian.ornl.gov/documentation/md_Doxygen_Installation.html. 196 | 197 | _Optimization and general numerics_ 198 | ``` 199 | $ pip3 install scipy 200 | $ pip3 install numpy 201 | ``` 202 | 203 | _Parallelization_ 204 | ``` 205 | $ pip3 install mpi4py 206 | ``` 207 | 208 | _Visualization and tabulation_ 209 | ``` 210 | $ pip3 install matplotlib 211 | $ pip3 install tabulate 212 | ``` 213 | 214 | ## Package creation and installation 215 | A package can be created for distribution purpose: 216 | ```shell 217 | $ python3 setup.py sdist 218 | ``` 219 | This produces a file in `dist` directory that can be installed in a virtual environment as follows: 220 | ```shell 221 | $ pip3 install HDMR-0.0.1.tar.gz #replace 0.0.1 with appropriate version 222 | ``` 223 | Using such package installation, requirements will be installed automatically. 224 | 225 | ## Support 226 | This work is generously supported by grants from the [Swiss National Science Foundation](https://www.snf.ch) under project IDs “New methods for asset pricing with frictions”, "Can economic policy mitigate climate change", the [Enterprise for Society (E4S)](https://e4s.center), and Emmanuel Jeanvoine from UNIL's [DCSR](https://www.unil.ch/ci/fr/home/menuinst/calcul--soutien-recherche.html). 227 | -------------------------------------------------------------------------------- /examples/analytical/unit_test.py: -------------------------------------------------------------------------------- 1 | # add root into path 2 | import os 3 | import sys 4 | import time 5 | 6 | import numpy as np 7 | from tabulate import tabulate 8 | from DDSG import DDSG 9 | 10 | # get parameters from command line 11 | d = int(sys.argv[1]) 12 | l_max = int(sys.argv[2]) 13 | k_max = int(sys.argv[3]) 14 | 15 | domain =np.zeros(shape=(d,2)) 16 | domain[:,1]=1 17 | 18 | # assuming a computaionaly expensive function call 19 | def f_example_heavey(X): 20 | n = X.shape[0] 21 | val = np.zeros(n); 22 | 23 | for i in range(0,n): 24 | val[i]=-15*np.sum( np.abs(X[i,:]-4/11)) 25 | time.sleep(0.01) 26 | 27 | return val 28 | 29 | x0=np.ones(shape=(1,d))/2.0 30 | ddsg = DDSG() 31 | ddsg.init(f_example_heavey,d) 32 | ddsg.set_grid(domain=domain,l_max=l_max,eps_sg=1e-6) 33 | ddsg.sg_prl=True 34 | 35 | if k_max>0: 36 | ddsg.set_decomposition(x0,k_max=k_max,eps_rho=1e-6,eps_eta=1e-6) 37 | 38 | [err_max,err_l2,num_grid_points,num_func,t_build,t_eval,t_orical]= ddsg.benchmark(N=1000,verbose=4) 39 | 40 | if(ddsg.proc_rank==0): 41 | headers = ['Error-Max','Error-L2','#Grid Points','#Comp. Func.','Time-Build','Time-Eval','Time-Orical'] 42 | print('\n### Benchmark Results') 43 | print(tabulate([[err_max,err_l2,num_grid_points,num_func,t_build,t_eval,t_orical]] ,headers=headers)) 44 | -------------------------------------------------------------------------------- /examples/irbc/tutorial.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Solving the IRBC Model with Adaptive SG and DDSG\n", 8 | "The material presented here supplements the work described in **[1]**, which introduces a highly performant time-iteration method for solving large-scale IRBC models. Embedded in the time-iteration solution method, the introduced DDSG algorithm is a highly scalable function approximation technique. The utilization of DDSG within the time-iteration has been shown to be highly effective in solving large-scale dynamic stochastic economic models. This algorithm was originally developed in C++ and Fortran using hybrid parallelism (OpenMP and MPI). However, the MPI parallel Python implementation presented here is intended to be more practical. The _DDSG_ class (see DDSG/DDSG.py) for function approximation (used for both DDSG and adaptive SG), and the _IRBC_ class (see IRBC/IRBC.py) for the IRBC model are required for the solution methods described in this notebook.\n", 9 | "\n", 10 | "In this notebook we shows the basics of the IRBC model, solve the model with time-iteartion using adaptive SG and finally solving it using the introduced DDSG algorithem. For details on the model we refer the reader to both **[1]**, and **[2]** and also to hands on examples in **[3]**.\n", 11 | "\n", 12 | "*References*\n", 13 | "- **[1]** [High-Dimensional Dynamic Stochastic Model Representation, A. Eftekhari, S. Scheidegger, SIAM Journal on Scientific Computing (SISC), 2022](https://epubs.siam.org/doi/10.1137/21M1392231)\n", 14 | "- **[2]** [Sparse Grids for Dynamic Economic Models J. Brumm, C. Krause, A. Schabb, S. Scheidegger](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3979412)\n", 15 | "- **[3]** https://github.com/SparseGridsForDynamicEcon/SparseGrids_in_econ_handbook" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 10, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "import time\n", 32 | "import numpy as np\n", 33 | "from tabulate import tabulate\n", 34 | "\n", 35 | "import os\n", 36 | "import sys\n", 37 | "import matplotlib.pyplot as plt\n", 38 | "\n", 39 | "from IRBC import IRBC\n", 40 | "from DDSG import DDSG\n", 41 | "from scipy import optimize" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "## 1. IRBC Model \n", 49 | "We begin by first initilizing the IRBC model which follows the description of the two models (_smooth_ and _non-smooth_) described in **[2]**. With the default parameters set, we can initialize the model using the following steps:\n", 50 | "1. Instatitate the IRBC model with the number of countries (say 2) and type of model (smooth or non-smooth), i.e., `model=IRBC(num_countries=2,irbc_type='non-smooth')`.\n", 51 | "2. Set the parameters of the model, i.e.,`.set_parameters()`.  Note, all patamersts are set by defualt to those found in **[2]**.\n", 52 | "3. Set the defualt integral rules, i.e., `.set_integral_rule()`. Notethat at the moment, only 'monomials_power' has been implemented and this is selected by default.\n", 53 | "4. (optional) For confirmation, we can print the parameters of the model using `print_parameters()` method.\n", 54 | "\n", 55 | "The main method of the IRBC object is the `.system_of_equations(x,state,p_last)` which is the value of the residual of the first-order-condtions optimiality conditions of the model. In particular, `x` is the policy to be solved, `state` is the state, and `p_last` is the last best estimate of the policy function (i.e., the current policy function). The zeros of this system of (non-linear) equations must be solved for all discrete states (i.e., grid points) in the statespace. This procedure of finding the policies that correspond to the equilibrium condition of the model at some states is encapsulated in the function `eq_condition(X)`. Note `eq_condition_init_guess(X)` is simply the intial guess of the policy function." 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 11, 61 | "metadata": {}, 62 | "outputs": [ 63 | { 64 | "name": "stdout", 65 | "output_type": "stream", 66 | "text": [ 67 | "Parameter Variable Value\n", 68 | "------------------------------------------------------ ---------- -------------------------------\n", 69 | "Intertemporal elasticity of substitution(IES) gamma ies_a+(i-1)(ies_b-ies_a)/(N-1)\n", 70 | "IES factor a ies_a 0.25\n", 71 | "IES factor b ies_b 1\n", 72 | "Discount factor beta 0.99\n", 73 | "Capital share of income zeta 0.36\n", 74 | "Depreciation rate delta 0.01\n", 75 | "Persistence of total factor productivity shocks rho_Z 0.95\n", 76 | "Standard deviation of total factor productivity shocks sig_E 0.01\n", 77 | "Intensity of capital adjustment costs kappa 0.5\n", 78 | "Lower bound for capital k_min 0.8\n", 79 | "Upper bound for capital k_max 1.2\n", 80 | "Aggregate productivity A_tfp 0.05583613916947258\n", 81 | "Welfare weight pareto [9.71989390e-06 5.58361392e-02]\n", 82 | "Lower bound for total factor productivity a_min -0.15999999999999986\n", 83 | "Upper bound for total factor productivity a_max 0.15999999999999986\n" 84 | ] 85 | } 86 | ], 87 | "source": [ 88 | "model = IRBC(num_countries=2, irbc_type='non-smooth') \n", 89 | "model.set_parameters()\n", 90 | "model.set_integral_rule()\n", 91 | "model.print_parameters() \n", 92 | "\n", 93 | "def eq_condition(X): \n", 94 | " global p_last\n", 95 | " [n,d]=X.shape\n", 96 | " result = np.empty(shape=(n,model.grid_dof))\n", 97 | " for i in range(0,n):\n", 98 | " state = X[i,:]\n", 99 | " p_guess = p_last.eval(X[i,:].reshape(1,-1))\n", 100 | " solution = optimize.root(fun=model.system_of_equations, x0=p_guess,tol=1e-6,args=(state,p_last), method='hybr') \n", 101 | " result[i,:] = solution.x \n", 102 | " return result\n", 103 | "\n", 104 | "def eq_condition_init_guess(X):\n", 105 | " [n,d]=X.shape\n", 106 | " val = np.empty(shape=(n,model.grid_dof))\n", 107 | " for i in range(0,n): \n", 108 | " val[i,0:model.num_countries] = (model.k_min + model.k_max)/2\n", 109 | " val[i,model.num_countries] = 1\n", 110 | " val[i,model.num_countries+1:] = -0.1\n", 111 | "\n", 112 | " return val" 113 | ] 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "metadata": {}, 118 | "source": [ 119 | "## 2. Time-Iteration\n", 120 | "Here we implement the time-iteration algorithm, which consists of an incremental update of the policy function using the previous estimate of the policy function.To generate a baseline estimate of the policy function, we use the DDSG library, but with only SG. We can do this by _not_ calling the `set_decomposition()` method; see _examples/analytical_ for more details and examples of using the _DDSG_ library. Notice that `p_next` and `p_last` are the global variables that are called within `eq_condition`. After each iteration, we swap the policy and proceed to the next iteration. \n", 121 | "\n", 122 | "This baseline estimate is a crude approximation of the plocly function with refinement up to only level 1. The estimated policy function is saved to file using the 'dump()' method. We will reload it and use it to solve the optimal policy using adaptive SG and DDSG in parts 2.1 and 2.2, respectively." 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 12, 128 | "metadata": {}, 129 | "outputs": [ 130 | { 131 | "name": "stdout", 132 | "output_type": "stream", 133 | "text": [ 134 | "#time-iteration:\n", 135 | "0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 " 136 | ] 137 | } 138 | ], 139 | "source": [ 140 | "# Max time-iteration iterations\n", 141 | "iter_max = 300\n", 142 | "\n", 143 | "# ASG parametrs\n", 144 | "eps_sg = 0\n", 145 | "l_max = 1\n", 146 | "\n", 147 | "# Grid Domain Parameters\n", 148 | "domain = np.zeros((model.grid_dim,2))\n", 149 | "domain[0:model.num_countries,0] = model.k_min\n", 150 | "domain[0:model.num_countries,1] = model.k_max\n", 151 | "domain[model.num_countries:,0] = model.a_min\n", 152 | "domain[model.num_countries:,1] = model.a_max\n", 153 | "\n", 154 | "# construct last grid\n", 155 | "# this will initially hold the guessed policy\n", 156 | "p_last = DDSG()\n", 157 | "p_last.init(f_orical=eq_condition_init_guess,d=model.grid_dim,m=model.grid_dof) \n", 158 | "p_last.set_grid(domain=domain,l_max=l_max,eps_sg=eps_sg)\n", 159 | "p_last.build(verbose=0)\n", 160 | "\n", 161 | "# \"course\" time-iteration \n", 162 | "print('#time-iteration:')\n", 163 | "for i in range(0,iter_max):\n", 164 | " \n", 165 | " p_next = DDSG()\n", 166 | " p_next.init(eq_condition,d=model.grid_dim,m=model.grid_dof) \n", 167 | " p_next.set_grid(domain=domain,l_max=l_max,eps_sg=eps_sg)\n", 168 | " p_next.build(verbose=0)\n", 169 | " print(i,' ',end='')\n", 170 | "\n", 171 | " # swap policy\n", 172 | " p_last = p_next\n", 173 | "\n", 174 | "p_next.dump('p_baseline',replace=True)" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "### 2.1 Time-Iteration with Adaptive SG\n", 182 | "Following the baseline estimate in section 2.0, we load it using `p_last = DDSG('p_baseline')` and continue with the time-iteration using adaptive SG, but this time with a higher refinement level. \n", 183 | "To evaluate the time-iteration statgnation, we sample the policy function with `X_sample` and return the L2-nrom." 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": 13, 189 | "metadata": {}, 190 | "outputs": [ 191 | { 192 | "name": "stdout", 193 | "output_type": "stream", 194 | "text": [ 195 | "#time-iteration:0 time(Sec.):9.13e+00 error_l2:5.97e-05 gridpoints:1.04e+03\n", 196 | "#time-iteration:1 time(Sec.):4.73e+00 error_l2:9.93e-06 gridpoints:1.04e+03\n", 197 | "#time-iteration:2 time(Sec.):4.34e+00 error_l2:8.81e-06 gridpoints:1.04e+03\n", 198 | "#time-iteration:3 time(Sec.):4.46e+00 error_l2:7.51e-06 gridpoints:1.05e+03\n", 199 | "#time-iteration:4 time(Sec.):8.23e+00 error_l2:6.76e-06 gridpoints:1.06e+03\n" 200 | ] 201 | } 202 | ], 203 | "source": [ 204 | "# Max time-iteration iterations\n", 205 | "iter_max = 5\n", 206 | "\n", 207 | "# SG parametrs\n", 208 | "l_max = 5\n", 209 | "eps_sg = 1e-3\n", 210 | "\n", 211 | "# sample points int the domain\n", 212 | "X_sample = np.random.uniform(low=domain[:,0],high=domain[:,1],size=(1000,model.grid_dim))\n", 213 | "\n", 214 | "# results\n", 215 | "t_total_sg =[]\n", 216 | "grid_points_sg =[]\n", 217 | "error_l2_mean_sg =[]\n", 218 | "\n", 219 | "p_last = DDSG('p_baseline')\n", 220 | "# time-iteration\n", 221 | "for i in range(0,iter_max):\n", 222 | " \n", 223 | " # construct sparse grid \n", 224 | " t_total_sg.append(-time.time())\n", 225 | "\n", 226 | " p_next = DDSG()\n", 227 | " p_next.init(eq_condition,d=model.grid_dim,m=model.grid_dof) \n", 228 | " p_next.set_grid(domain=domain,l_max=l_max,eps_sg=eps_sg)\n", 229 | " p_next.build(verbose=0)\n", 230 | " \n", 231 | " t_total_sg[-1] += time.time()\n", 232 | "\n", 233 | " diff = p_next.eval(X_sample) - p_last.eval(X_sample)\n", 234 | " error_l2_mean_sg.append(np.linalg.norm(diff.flatten())/diff.size) \n", 235 | " grid_points_sg.append(p_next.num_grid_points)\n", 236 | "\n", 237 | " print('#time-iteration:{:d} time(Sec.):{:.2e} error_l2:{:.2e} gridpoints:{:.2e}'.format(i,t_total_sg[-1],error_l2_mean_sg[-1],grid_points_sg[-1]) )\n", 238 | " \n", 239 | " # swap policy\n", 240 | " p_last = p_next\n", 241 | "\n", 242 | "p_next.dump('p_sg',replace=True)" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "metadata": {}, 248 | "source": [ 249 | "### 2.2 Time-Iteration with DDSG\n", 250 | "Following the baseline estimate in section 2.0, we load it using `p_last = DDSG('p_baseline')` and continue with the time-iteration using adaptive DDSG with expansion order of 1. \n", 251 | "As before, we evaluate the time-iteration statgnation, we sample the policy function with `X_sample` and return the L2-nrom." 252 | ] 253 | }, 254 | { 255 | "cell_type": "code", 256 | "execution_count": 14, 257 | "metadata": {}, 258 | "outputs": [ 259 | { 260 | "name": "stdout", 261 | "output_type": "stream", 262 | "text": [ 263 | "#time-iteration:0 time(Sec.):4.34e+00 error_l2:8.12e-06 gridpoints:8.90e+01\n", 264 | "#time-iteration:1 time(Sec.):6.04e-01 error_l2:6.61e-06 gridpoints:8.90e+01\n", 265 | "#time-iteration:2 time(Sec.):3.02e+00 error_l2:5.56e-06 gridpoints:9.30e+01\n", 266 | "#time-iteration:3 time(Sec.):6.71e-01 error_l2:4.94e-06 gridpoints:9.30e+01\n", 267 | "#time-iteration:4 time(Sec.):6.28e-01 error_l2:4.66e-06 gridpoints:9.30e+01\n" 268 | ] 269 | } 270 | ], 271 | "source": [ 272 | "# Max time-iteration iterations\n", 273 | "iter_max = 5\n", 274 | "\n", 275 | "# DDSG parametrs\n", 276 | "k_max = 1\n", 277 | "l_max = 5\n", 278 | "eps_sg = 1e-3\n", 279 | "eps_dd = 1e-6\n", 280 | "\n", 281 | "x0=np.mean(domain,axis=1).reshape((1,domain.shape[0]))\n", 282 | "\n", 283 | "t_total_ddsg =[]\n", 284 | "grid_points_ddsg =[]\n", 285 | "error_l2_mean_ddsg =[]\n", 286 | "\n", 287 | "p_last = DDSG('p_baseline')\n", 288 | "for i in range(0,iter_max):\n", 289 | " \n", 290 | " t_total_ddsg.append(-time.time())\n", 291 | "\n", 292 | " p_next = DDSG()\n", 293 | " p_next.init(eq_condition,d=model.grid_dim,m=model.grid_dof) \n", 294 | " p_next.set_grid(domain=domain,l_max=l_max,eps_sg=eps_sg)\n", 295 | " p_next.set_decomposition(x0,k_max=k_max,eps_rho=eps_dd,eps_eta=eps_dd)\n", 296 | " p_next.build(verbose=0)\n", 297 | " \n", 298 | " t_total_ddsg[-1] += time.time()\n", 299 | "\n", 300 | " diff = p_next.eval(X_sample) - p_last.eval(X_sample)\n", 301 | " error_l2_mean_ddsg.append(np.linalg.norm(diff.flatten())/diff.size) \n", 302 | " grid_points_ddsg.append(p_next.num_grid_points)\n", 303 | "\n", 304 | " print('#time-iteration:{:d} time(Sec.):{:.2e} error_l2:{:.2e} gridpoints:{:.2e}'.format(i,t_total_ddsg[-1],error_l2_mean_ddsg[-1],grid_points_ddsg[-1]) )\n", 305 | "\n", 306 | " # swap policy\n", 307 | " p_last = p_next\n", 308 | "\n", 309 | "p_next.dump('p_ddsg',replace=True)" 310 | ] 311 | }, 312 | { 313 | "cell_type": "markdown", 314 | "metadata": {}, 315 | "source": [ 316 | "## 3 Error Metrics\n", 317 | "We present two types of metrics here. The first metric is the stagnation metric, which does not always correspond to the convergence. On each iteration, it is simply the L2 norm of the sampled policy function. This metric is shown below for experience in sections 2.1 and 2.2 in terms of cumulative runtime time and cumulative grid points. \n", 318 | "\n", 319 | "_The runtime difference (and grid points) between SG and DDSG grows significantly with increasing dimension, with DDSG being buch less._" 320 | ] 321 | }, 322 | { 323 | "cell_type": "code", 324 | "execution_count": 15, 325 | "metadata": {}, 326 | "outputs": [ 327 | { 328 | "data": { 329 | "image/png": "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L+hIREREqtawoR8e/txz8M03QUcjItlARVKWO+00aNTID2MQERFJVSUlUF4enwtbiojUREVSlmvVCo4/3hdJGnInIiKp6oAD/PmzkycrX4lI4qlIEoqK/HSPCbz+roiISL2VlMDixTBvXtCRiEimU5EkGnInIiJp4dxzITdXEziISOKpSBL22QeOO06z3ImkqwYNGjBgwAAOOuggDjnkEG666SZ27doFQGlpKfn5+QwcOJD999+fkSNH8swzz+zedsmSJRQWFjJgwAD69u3LuHHjdj/27rvvUlhYSJ8+fTj00EM55ZRTWLhwYdLbJxKWnw9nngkPPQRbtwYdjYjEqqY81bVr1915atiwYSmRp3SdJAH8LHc/+Qm8/z4MGhR0NCJSG02bNmX+/PkArFmzhvPPP5+NGzcyYcIEAI455pjdCeett96iuLiYpk2bMmLECH7xi1/wy1/+ktNOOw1gd3JZvXo1Z599Ng899BBHHnkkAG+++Saff/45/fr1S3ILRSqUlPgi6emn/UVmRST11ZSnjjjiCGbOnAnA/PnzOf300wPPU+pJEgBOP91PsaohdyKJNXXhVApuLSBnQg4FtxYwdWF8p+pq3749kyZN4s4778RV0TXcv39/fv/733PnnXcCsGrVKrp27br78XBiufPOOxkzZszuxANw9NFHc/rpp8c1XpHaOvZY6NZNQ+5EEiXoPDVgwICUyFMqkgSA1q1hxAjNcieSSFMXTmXcjHEs27AMh2PZhmWMmzEu7glo3333pby8nDVr1lT5+KGHHsonn3wCwC9/+UuOPfZYTjrpJG655RbWr18PwOLFizn00EPjGpdIPDRo4K+Z9OKLsHJl0NGIZBblqQoabie7FRXBT38KH3wA+t9IpPaumHkF87+ZH/XxOSvmsK18W6V1m3ds5idP/YR/zPtHldsM6DiAW0feGscoqfTN3dixYznxxBOZOXMmTz31FPfccw8LFizYa5shQ4awceNGTjjhBG677ba4xiNSWyUlMHEiPPAA/OY3QUcjkj6Up2KXUUWSmY0CRnXp0oXS0tKEvU5ZWVlC9x+Utm0bkpNzFDfdtJyLL/6y0mOZ2uZosq29oDbXVX5+Pps2bQJg+/btlJeXR33unokncn207bZv3757/9WJfM6XX35JTk4OTZs2ZfPmzezcuXP34+Xl5cyePZs+ffrsXpeXl0dRURFFRUUMGTKEd955h969ezN79myOPfZYAF5++WWefPJJZs6cWWU8W7duzbr3T13UNk9l499lWE1t799/AH//e2OGDHkXs+TFlQw67qVBhxGIRLU9HfKUc67S4ymRp5xzGbcMGjTIJdKsWbMSuv8gHX+8c717O7drV+X1mdzmqmRbe51Tm+vqo48+ivm5PW7p4biOvZYet/SoVwzNmzfffXvNmjXu+OOPd7///e+dc76Np5xyyu7H3377bVdQUOBefvll55xzzz//vNu+fbtzzrlVq1a5jh07ulWrVrlVq1a57t27u7feemv3tlOmTHFjxoypMoaqfg/AXJcCOSEVl1jzVDb+XYbV1PZ//tM5cO7tt5MTTzLpuGenRLU9HfLUiSeeuPvxBQsWpESeyqieJKm/oiIYNw4WLIABA4KORiSzTBwxkXEzxrF5x+bd65o1asbEERPrtd8tW7YwYMAAduzYQcOGDbnwwgu58sordz/+xhtvMHDgQDZv3kybNm24/fbbGTFiBAAvvvgil19+Obm5uQDceOONdOzYEYBHHnmEq6++mpUrV9K+fXvatm3L73//+3rFKhIvRUVw2WV+Aocjjgg6GpHMEFSemj179u481b59+5TIUyqSpJIzzoCf/cxP4KAiSSS+ivsVAzD+lfEs37Cc7vndmThi4u71dVXd0InCwkI2bNiw+/6mTZvIy8vbff/mm2/m5ptvrnLboUOH8tprr9UrNpFEycvzl6945BG49VZo1izoiETSX1B5asWKFZVyU6Sg8pSKJKmkbVsYPtwXSX/8Ixk3zlskaMX9iuudbETEGzsW7r8fnngCivVnJRIXylOepgCXvRQVwX/+Ax9+GHQkIiIi0Q0bBj176ppJIhJ/KpJkL6efDjk5MH160JGIiIhEl5MDY8bAq6/CsmVBRyMimURFkuylfXsoLNSFZUVi5bL8DyXb2y/BGjPG56r77w86EpHUle2f03Vpv4okqVJRESxZAosWBR2JSGrLzc1l3bp1WZuAnHOsW7du96xDIslWUODPpb3vPn2xJ1IV5am65SlN3CBVOvNMuPRS35vUr1/Q0Yikrq5du7JixQq+/fbboEOJydatW+Ne0OTm5tK1a9e47lOkNsaOhYsugjfe8OcpiUiFdMhTichNkeqSp1QkSZXat4cf/cgXSRMmBB2NSOpq1KgRPXv2DDqMmJWWljJw4MCgwxCJq/AXe5Mnq0gS2VM65KlUzE0abidRjR4Nn3wCH30UdCQiIiLRNW8OZ5/tv9grKws6GhHJBCqSJKozz/TXSfr3v4OOREREpHpjx8IPP2hmVhGJDxVJElXHjn7YgookERFJdUceCX36+AkcRETqS0WSVKuoyA+3W7q0WdChiIiIRGUGJSXw2mvwxRdBRyMi6U5FklTrrLN84nnttXZBhyIiIlKtiy7yOUu9SSJSXyqSpFodO8Ixx0BpafugQxEREalW165w/PEwZQrs2hV0NCKSzlQkSY1Gj4alS5vz8cdBRyIiIlK9sWNh+XKYNSvoSEQknalIkhr5IXdOMwaJiEjKO+00yM/XkDsRqR8VSVKjzp3h4IM3aJY7ERFJeU2bwnnnwWOPwYYNQUcjIulKRZLE5Ec/+paFC2HJkqAjERERqd7YsbBlCzz6aNCRiEi6UpEkMRk27FtA10wSEZHUd9hh0LevhtyJSN2Zcy7oGOLGzEYBo7p06XLxgw8+mLDXKSsro0WLFgnbfyoqKyvjN785hq1bG3DvvXODDifhsvUYq82ZL5ltHj58+Dzn3OCkvFiaqG2eysb3aFh92z5tWjfuuacXU6a8Q/fuW+IYWeLpuKvt2SaotleXpzKqSAobPHiwmzs3cf/Il5aWUlhYmLD9p6LS0lLmzy/kl7+ETz/1VzXPZNl6jNXmzJfMNpuZiqQoYs1T2fgeDatv21etgm7d4Kqr4E9/il9cyaDjXhh0GIFQ2wuT/rrV5SkNt5OYjR7tf2rInYiIpLpOnWDkSLj/figvDzoaEUk3KpIkZl27whFHqEgSEZH0MHYsrFwJL70UdCQikm5UJEmtFBXB/Pnw2WdBRyIiIlK9H/8YWrfWBA4iUnsqkqRWzjrL/9SFZUVEJNU1aQLFxfDkk/D990FHIyLpREWS1Er37jBkiIbciYhIeigpgW3b4OGHg45ERNKJiiSptaIieP99+OKLoCMRERGp3sCB0L+/htyJSO2oSJJa0yx3IiKSLsz8BA7vvQeLFwcdjYikCxVJUms9esDhh6tIEhGR9FBcDA0bqjdJRGKnIknqpKgI5s3TkDsREUl97dr5me4eeAB27Ag6GhFJByqSpE7Cs9w99liwcYiIiMSipARWr4aZM4OORETSgYokqZOePWHwYA25ExGR9HDyydC+vYbciUhsVCRJnRUV+RNhly4NOhIREZHqNWoEF1wAM2bA2rVBRyMiqU5FktRZUZH/qQvLiohIOigp8eckPfRQ0JGISKpTkSR11rMnDBqkIXciIpIe+vXzeWvy5KAjEZFUpyJJ6mX0aHj3XVi2LOhIREREalZSAvPn+0VEJBoVSVIv4SF3muVORETSwfnnQ+PGmsBBRKqnIknqpVcvGDhQQ+5ERCQ9tG4Np50GU6fC9u1BRyMiqUpFktRbURHMmQNffRV0JCIiIjUrKfEz3D37bNCRiEiqUpEk9TZ6tP+pWe5ERCQdnHACdOqkCRxEJDoVSVJvffrAIYdoyJ2IiKSHhg3hwgvhuefgm2+CjkZEUpGKJImLoiKYPRtWrAg6EhERkZqNHQvl5f7cJBGRPalIkrjQLHciIpJODjgAhg71Q+6cCzoaEUk1KpIkLvbbD/r315A7ERFJHyUlsHgxzJsXdCQikmpUJEncFBXBW2/BypVBRyIiIlKzc86B3FxN4CAie1ORJHGjIXciIpJOWrWCM86Ahx6CrVuDjkZEUomKJImb/feHgw/WVOAiIpI+xo6F9evh6aeDjkREUom5DDpb0cxGAaO6dOly8YMPPpiw1ykrK6NFixYJ238qirXNU6b0YMqUAv7979m0aZO+lzLXMc4OanNiDR8+fJ5zbnBSXixN1DZPZeN7NCxZbS8vh/PPH0pBwQ/8+c8LE/56sdBxV9uzTVBtrzZPOecybhk0aJBLpFmzZiV0/6ko1jZ/9JFz4NwddyQ2nkTTMc4OanNiAXNdCuSEVFxizVPZ+B4NS2bbx493LifHuRUrkvaS1dJxz05qe/JVl6c03E7iqm9fOOggzXInIiLpo6QEdu2CBx4IOhIRSRUqkiTuiorgjTdg1aqgIxEREalZ795w9NG6ZpKIVFCRJHE3erRPMk88EXQkIiIisRk7Fj79FObMCToSEUkF1RZJZnaEmf3NzD40s2/NbLmZPWdml5pZfrKClPRy0EF+2J2G3IlIoilPSbwUFUGzZrpmkoh4UYskM3se+CnwAjAS6AQcCFwL5AJPmdmpyQhS0k9REbz+OqxeHXQkIpKplKcknvLy/EiIRx6BzZuDjkZEglZdT9KFzrmfOOeeds597Zzb6Zwrc86975y7yTlXCLydpDglzRQV+ZNgH3886EhEJIMpT0lcjR0LGzdquLiIVFMkOefWRt43s5Zm1jq8VPUckbCDDoIDDtCQOxFJHOUpibdhw6CgQEPuRCSGiRvM7L/N7BvgQ2BeaJmb6MAkvZn5YQuvvQZr1gQdjYhkMuUpiZecHD8d+KuvwrJlQUcjIkGKZXa7XwEHO+cKnHM9Q8u+iQ5M0l94yJ2GLYhIgilPSdyMGeNnaL3//qAjEZEgxVIkfQ7oFEaptX79YL/9NORORBJOeUripqAAhg+H++7TNZNEslnDGJ7zW+BtM3sH2BZe6Zz7RcKikoxg5nuTrr8evv0W2rULOiIRyVDKUxJXY8fCRRf5C6MPGxZ0NCIShFh6ku4BXgXmUDHWe14ig5LMoSF3IpIEylMSV2ee6acE1wQOItkrlp6kRs65KxMeiWSk/v2hTx8/5G7cuKCjEZEMpTwlcdW8OZx9NkybBnfcAS1aBB2RiCRbLD1Jz5vZODPrtOfUqiI1Cc9yN2sWrNVEvCKSGMpTEndjx8IPP8D06UFHIiJBiKVIOo/QeG80tarUQVERlJfDk08GHYmIZCjlKYm7I4/0IyHuuy/oSEQkCNUWSWaWA/wmYkpVTa0qtTZgAPTqpVnuRCT+lKckUcz8NZNeew2++CLoaEQk2aotkpxzu4BfJykWyVDhWe5eeQXWrQs6GhHJJMpTkkgXXuhzmHqTRLJPLMPtXjazX5lZN431lrrSkDsRSSDlKUmIbt3g+ONhyhQ/U6uIZI9YiqRzgEuB19FYb6mjgQOhZ08NuRORhFCekoQZOxaWL/cTEIlI9qhxCnDnXM9kBCKZLTzk7uab4bvvoLW+4xWROFGekkQ67TTIz/dD7kaMCDoaEUmWGnuSzKyRmf3CzKaHlp+bWaNkBCeZpagIdu6Ep54KOhIRySTKU5JITZvCeefBY4/Bhg1BRyMiyRLLcLu7gEHA30PLoNA6kVoZNAgKCjTkTkTiTnlKEqqkBLZsgUcfDToSEUmWGofbAYc55w6JuP+qmS1IVECSucJD7m69Fb7/HvbZJ+iIRCRDKE9JQh1+OPTt64fcXXxx0NGISDLE0pNUbma9wnfMbF+gPHEhSSYbPRp27NCQOxGJK+UpSSgzP4HD22/DkiVBRyMiyRBLkfRrYJaZlZrZa8CrwP8kNizJVIcdBj16wPTpQUciIhlEeUoS7oILoEEDPx24iGS+WGa3e8XM+gD7h1Ytcc5tS2xYkqnMfG/S7bfD+vXQqlXQEYlIulOekmTo1AlGjoT774f/+z9fMIlI5orak2Rmw8ILMARoFVqGhNaJ1ElRkR9y9/TTQUciIulMeUqSraQEVq6El14KOhIRSbTqepJ+XcU6B/QHugH6DkXq5PDD/VXM//1vuOiioKMRkTSmPCVJNWqUv87ffff5XiURyVxRiyTn3KjI+2Z2FHAt8A1wWYLjkgwWHnL3t7/5a07k5wcdkYikI+UpSbYmTaC4GCZN0iytIpkulovJjjCzUuCPwM3OuaHOuRkJj0wyWlERbN8OM/ROEpF6Up6SZCopgW3bYNq0oCMRkUSq7pykU8zsbeBXwLXOueHOOY3ClbgYMgS6dtWFZUWk7pSnJAgDB0L//jB5ctCRiEgiVdeTNAPoCuwErjKzpyOX5IQnmSonxw+5e+EF2Lgx6GhEJE0pT0nSmfnepPfeg8WLg45GRBKluokbhictCslKRUVw661+yF1xcdDRiEgaUp6SQFxwAVx1lZ/A4cYbg45GRBKhuokbXttznZkd6px7P7EhSbYYOhS6dPFD7lQkiUhtKU9JUNq1gx//GB54AP70J2jUKOiIRCTeapy4YQ/3JiQKyUo5OXDWWTBzpobciUjcKE9JUpSUwOrVPoeJSOapbZFkCYlCslZRkZ8l6Nlng45ERDKE8pQkxckn+x6l++4LOhIRSYTaFkkTEhKFZK0jj4TOnTXLnYjEjfKUJEWjRv7cpBkzYO3aoKMRkXgz51z1TzBr5Jzbsce6ts65lPtIMLNRwKguXbpc/OCDDybsdcrKymjRokXC9p+KEtnm22/vzbPPduLJJ9+madPyhLxGbekYZwe1ObGGDx8+zzk3ONGvk8l5Khvfo2Hp0PYvvmjOT35yGD//+X8466yVcdtvOrQ9UdR2tT2Zqs1TzrkqF/ysQSuAtcCLQEHEY+9H2y4VlkGDBrlEmjVrVkL3n4oS2ebXX3cOnHv44YS9RK3pGGcHtTmxgLkugZ/12ZCnsvE9GpYubR80yLkBA+K7z3RpeyKo7dkpqLZXl6eqG273F+BE51xbYBLwkpkNDT2mMd8SN0ceCR07asidiNSa8pQErqQE5s/3i4hkjuqKpMbOucUAzrnpwOnAFDM7Hah+jJ5ILTRo4Ge5e+45KCsLOhoRSSPKUxK4886Dxo01gYNIpqmuSNphZh3Dd0KJaARwHdAnwXFJlikqgq1bfaEkIhIj5SkJXJs2cOqpMHUqbN8edDQiEi/VFUm/ATpErnDOrQB+BNyQyKAk+xx9NHTooCF3IlIrylOSEsaO9TPc6XIWIpkjapHknHvZObegivUbgP4JjUqyTnjI3bPPwg8/BB2NiKQD5SlJFSecAJ06weTJQUciIvFS2+skhR0R1yhEgNGjYcsWDbkTkbhQnpKkadgQLrzQ569vvgk6GhGJh7oWSSJxN2wYtG8P06cHHYmIiEjtlJRAebk/N0lE0l/DaA+Y2aHRHgIaJSYcyWYNGsCZZ8L998PmzdCsWdARiUgqU56SVNK3LwwZ4ofcXXklmCahF0lrUYsk4KZqHvsk3oGIgJ/l7u674fnn/TlKIiLVUJ6SlDJ2LFxyCcybB4MHBx2NiNRH1CLJOTc8mYGIgB9y166dn+VORZKIVEd5SlLNOefAFVf43iQVSSLpLeo5SWZ2dHUbmllLMzs4/iFJNmvY0A+5e+YZP4mDiEg0ylOSalq1gjPOgIce8tf+E5H0Vd3EDWeZ2dtm9nszO8XMDjezYWb2X2b2APAM0DRJcUoWGT3aTwP+/PNBRyIiKU55SlLO2LGwfj08/XTQkYhIfVQ33O6XZtYaOAsoAjoBW4CPgXucc28mJ0TJNoWF0Latn+XuzDODjkZEUpXylKSiY4+Fbt38kLuzzw46GhGpq+ombsA59x3wj9AikhQNG/rhCg8/7IfcNdX3wCIShfKUpJoGDeCii+D662HlSujSJeiIRKQudJ0kSUlFRVBWBi+8EHQkIiIitVNSArt2wQMPBB2JiNSViiRJSYWF0KaNn+VOREQknfTuDUcf7YfcORd0NCJSFyqSJCU1agSnnw4zZmiGIBERST9jx8Knn8KcOUFHIiJ1EVORZGZHmtn5ZnZReEl0YCJFRbBpE7z4YtCRiEiqU56SVFNUBM2a+d4kEUk/NRZJoWlU/wocDRwWWnSJNEm4Y4+F1q015E5Eqqc8JakoL89f0uKRR2Dz5qCjEZHaqnZ2u5DBwIHOaVStJFd4yN306bBtGzRpEnREIpKilKckJY0dC/ffD088AcXFQUcjIrURy3C7RUDHRAciUpWiIti4UUPuRKRaylOSkoYNg4ICDbkTSUex9CS1BT4ys3eBbeGVzrlTExaVSMixx0KrVn7I3ahRQUcjIilKeUpSUk6Onw58wgRYtgx69Ag6IhGJVSxF0nWJDkIkmsaN/ZC7xx/XkDsRieq6oAMQiWbMGLjuOj/s7ne/CzoaEYlVjcPtnHOvAZ8AeaHl49A6kaQID7l7+eWgIxGRVKQ8JamsoACGD4f77tM1k0TSSSyz250NvAsUAWcD75jZ6EQHJhJ23HEVQ+5ERPakPCWpbuxY+OILeOONoCMRkVjFMtxuPHCYc24NgJm1A14GpicyMJGwxo3htNPgqadg+3Z/X0QkgvKUpLQzz4RLL/UTOAwbFnQ0IhKLWGa3ywknnpB1MW4nEjdFRbB+vYbciUiVlKckpTVvDmef7UdElJUFHY2IxCKWJDLTzF4wsxIzKwGeBZ5LbFgilR13HLRsqSF3IlIl5SlJeWPHwg8/+Gv/iUjqi2Xihl8Dk4D+oWWSc+7qRAcmEqlJEz/k7skn/ZA7EZEw5SlJB0ceCX36+AkcRCT1xTQcwTn3mHPuytDyRKKDEqlKeMjdq68GHYmIpBrlKUl1Zv6aSa+95idxEJHUFrVIMrM3Qz83mdnGiGWTmW1MXogi3gknaMidiFRQnpJ0c+GFvlhSb5JI6otaJDnnjg79zHPOtYxY8pxzLZMXoojXpAmceqofcrdjR9DRiEjQlKck3XTrBscfD1OmwK5dQUcjItWJ5TpJD8SyTiQZRo+G777TkDsRqaA8Jelk7FhYvhxmzQo6EhGpTiznJB0UecfMGgKDEhOOSPVOPBHy8jQ7kIhUojwlaeO00yA/X0PuRFJddeck/dbMNgH9I8d5A6uBp5IWoUiE3FwYNQqeeEJD7kSynfKUpKOmTeG88+Cxx2DDhqCjEZFoqjsn6XrnXB5w4x7jvNs4536bxBhFKikqgnXroLQ06EhEJEjKU5KuSkpgyxZ49NGgIxGRaGK5TtJvzWwfMzvczIaFl2QEJ1KVE0+EFi00y52IeMpTkm4OPxz69tWQO5FUFsvEDT8FXgdeACaEfl6X2LBEomvaFH78Yz/kbufOoKMRkaApT0m6MfMTOLz9NixZEnQ0IlKVWCZuuBw4DFjmnBsODATWJzIokZoUFcHatRpyJyKA8pSkoQsugAYN/HTgIpJ6YimStjrntgKYWRPn3CfA/okNS6R6J50EzZtrljsRAZSnJA116gQjR8L990N5edDRiMieYimSVphZK+BJ4CUzewpYlsigRGoSHnL3+OMaciciylOSnkpKYOVKeOmloCMRkT3FMnHDGc659c6564DfAf8ETk9wXCI1KiqCb7+F118POhIRCZLylKSrUaOgdWtN4CCSimLpScLMGphZZ+BLYD7QMZFBicTipJOgWTPNciciylOSnpo0geJiePJJ+P77oKMRkUixzG53Gf7CfC8Bz4aWZxIcl0iNmjWDU07xQ+40nlskeylPSTorKYFt22DatKAjEZFIsc5ut79z7iDnXL/Q0j/RgYnEoqgI1qyBN94IOhIRCZDylKStgQOhf3+YPDnoSEQkUixF0lfAhkQHIlIXJ5/sJ3HQkDuRrKY8JWnLzPcmvfceLF4cdDQiEhZLkfQFUGpmvzWzK8NLogMTiUXz5n7I3WOPacidSBZTnpK0dsEF0LChJnAQSSWxFEnL8eO8GwN5EYtISigqgtWr4c03g45ERAKiPCVprV07f1mLBx6AHTuCjkZEABrW9ATn3IRkBCJSVyefDLm5fsjdj34UdDQikmzKU5IJSkr8LHcvvAAtWgQdjYjUWCSZ2QzA7bF6AzAXuCd8lXORoLRo4Qulxx6D226DBg2CjkhEkkl5SjLBySf7HqXJk+Gyy4KORkRiPSepDPhHaNkIbAL2C90XCVxREXzzDbz9dtCRiEgAlKck7TVq5M9NmjEDNmxoFHQ4Ilmvxp4k4Ejn3GER92eY2XvOucPMTPOwSEr48Y8rhtwdc0zQ0YhIkilPSUYYOxZuuQVefrk9p50WdDQi2S2WnqQWZtY9fCd0OzxadntCohKppRYt4KST/JC7XbuCjkZEkkx5SjJCv34waBDMnNkx6FBEsl4sRdL/AG+a2SwzKwXeAH5lZs2BKYkMTqQ2Ro+Gr7/WkDuRLKQ8JRmjpAQ++yyP+fODjkQku9VYJDnnngP6AFdQcVXzZ51zPzjnbk1seCKxGzUKmjTRhWVFso3ylGSS886DRo126ZpJIgGLpScJfPLZHzgEONvMLkpcSCJ1k5cHI0dqyJ1IllKekozQpg0ceeRapk6F7RosKhKYGoskM/sDcEdoGQ78BTg1wXGJ1ElREaxcCXPmBB2JiCSL8pRkmpEjv2HtWnj22aAjEclesfQkjQZGAN8458biv6XLT2hUInU0ahQ0bqwhdyJZRnlKMsphh31Pp07+mkkiEoxYiqQtzrldwE4zawmsAbolNiyRumnZEk48EaZP15A7kSyiPCUZpUEDx4UXwnPP+WsAikjyxVIkzTWzVvgL8s0D3gdmJzIokfooKoIVK+Cdd4KORESSRHlKMk5JCZSXw9SpQUcikp1imd3u/znn1jvn7gaOB8aEhjOIpKRTT/VD7qZPDzoSEUkG5SnJRH37wpAhfsidc0FHI5J9Ypm44ZXwbefcUufch5HrEs3MCs3sDTO728wKk/W6kr7y8+GEE3yRpMQikvmUpyRTjR0LixfDvHlBRyKSfaIWSWaWa2atgbZmto+ZtQ4tBUCXWHZuZv8yszVmtmiP9SPNbImZfWZmv6lhNw4oA3KBFbG8rkhRESxfDu++G3QkIpIoylOS6c45B3JzNYGDSBCq60n6b/zY7gNCP8PLU8CdMe7/PmBk5AozawD8DTgJOBA4z8wONLN+ZvbMHkt74A3n3EnA1cCE2Jsm2ezUUyEnB44/3v8sKNC4bpEMpDwlGa1VKxg4EO66S7lMJNkaRnvAOXcbcJuZXeacu6MuO3fOvR76Ri/S4cBnzrkvAMxsGnCac+564MfV7O57oEld4pDsE762xKZN/ueyZTBunL9dXBxMTCISX8pTkummToX3368YOq5cJpI85qKctGFmhwFfOee+Cd2/CDgLWAZc55z7LqYX8MnnGefcwaH7o4GRzrmfhu5fCAxxzv08yvZnAicCrYC7nHOlUZ43DhgH0KFDh0HTpk2LJbw6KSsro0WLFgnbfypKtzafe+5QVq/O3Wt9hw5bmTat5ivNplt740Ftzg7JbPPw4cPnOecGJ2r/2ZCnsvE9Gqa2t4iay9q23cqjj87BLIDgEkzHXW1PpmrzlHOuygU/hWrr0O1hwNf45PN/wPRo21WxnwJgUcT90cC9EfcvBO6MdX+xLIMGDXKJNGvWrITuPxWlW5vNnPPfve29FBc7d8cdzs2d69z27VVvn27tjQe1OTsks83AXBfHz/Y9l2zIU9n4Hg1T26vPZa1aOTdsmHOXXurcPfc49/bbzm3cGGzc8aDjnp2Cant1eSrqcDuggav4Fu4cYJJz7jHgMTObH1t9VqWVVL7IX9fQOpG46d7dD0vYU9Om8MorFWO6mzaFwYNh6FC/HHEEdOqU3FhFpM6UpySjRctl++zjJ3X48EO4//6KoeUA++4L/fpB//4VP3v3hgYNkhe3SCaotkgys4bOuZ3ACEJDBGLYribvAX3MrCc+6ZwLnF+P/YnsZeJEP2578+aKdc2awaRJcP758NVXMHs2zJnjf956K+zY4Z/XvTv06nUg8+f7wmngQGiiswxEUpHylGS0aLnsjjsqzklyzhdSH37ol4UL/c8ZM2DXLv+c3Fw46KDKhVO/ftC+ffLbJJIuqksiDwOvmdlaYAvwBoCZ9QY2xLJzM3sYKMRPz7oC+INz7p9m9nPgBaAB8C/n3OK6N0Fkb+HkMX68nwq8e3efbMLru3f3yznn+Ptbt8L8+RWFU2lpS2bN8o81bgyHHup7mcI9Tt26kZFjwUXSjPKUZLSachn4XFRQ4JdTT61Yv2ULfPxxRdH04Yfw3HOVpxPv0GHvwunAA31RJZLtqpvdbmLoYnydgBdD4/bATxt+WSw7d86dF2X9c8BztYxVpFaKi2Of/Sc3t6IAAigtnUOfPoW8805F4XTXXXDLLf7xzp0rhucNHQqDBvmheyKSPMpTkg1qk8siNW3qv+A79NDK69esqVw4LVwIf/+7/7IQ/LC8/farKJzCxVOPHvpyULJLtcMRnHN7TQPmnPs0ceGIpI4uXeDMM/0CfjjeggUVQ/TmzIHHH/ePNWwIAwZULpx69lRCEUk05SmR2mnfHkaM8EtYeTl89lnlwum99+DRRyue07IlHHxw5cKpXz/Iz09+G0SSoT5jtkWySqNGfpKHwYPh56GJgNes8cVSeJk8Ge4MXcKyffvKE0IMHgxZOrOniIiksAYNYP/9/VJUVLF+0yZYtKjyuU7TpsHdd1c8p3v3yoVT//6+J6qh/sOUNKe3sEg9tG/vx4CHx4Hv3AmLF1f0NM2ZA08/7R/LyfHJI7Jw6tNHvU0iIpKa8vJ8rjriiIp1zsGKFZULp4ULYeZMnwPBn8t74IF7n+/UsaNynqQPFUkicdSwIRxyiF8uucSvW7cO3n23onB66KGKb+Fat64omoYOhcMP19AFERFJXWZ+8qJu3eCUUyrWb9sGn3xSuXB6+WU/RXlY27Z7F04HHeRn7BNJNSqSRBKsTRs46SS/gJ+S9eOPK5/b9Pzz/ts5M//tW+RMen37+l6oqVOrn+FIREQkKE2aVHxJGGndusqF04cfwj/+UTGtuZm/jlN4yJ5ZW7p18+f15uT45yj/SRCsYjKg9Gdmo4BRXbp0ufjBBx9M2OuUlZXRIstOLsm2Nie7vWVlDfjkk5Z89JFfPv64JRs3NgKgefOdtG+/leXLm1FenrN7myZNyvnVr5Zw3HFr4hRDdh1jUJsTbfjw4fOcc4OT8mJporZ5Khvfo2Fqe+a2fdcuWLWqKZ9/3pwvv2zO55+34Msvm7NyZVOc8+PxcnPL6dnzB5o0KWfRonx27kxc/ksVmX7cqxNU26vLUxlVJIUNHjzYzZ07N2H7Ly0tpbCwMGH7T0XZ1uag2+sc/Oc/FT1N//xnxcVuI3Xs6MeGx+NK6kG3OQhqc2KZmYqkKGLNU9n4Hg1T2wuDDiPpfvgB7r9/Ho0bD9rd8/TaaxUXxY3Upo0fyp5JM8lm63GH4NpeXZ7ScDuRFGTmZwfabz8YMwbuuafq533zjb8Y4Akn+OF8J56oK6iLiEh6at4c+vbdROT/yjk5VT933Tro1ctft/DooyuW/v3j88WhiIokkTTQvTssW7b3+rZt4eST/axCDz/si6tBgyrOgTr8cCULERFJX9HyX6dO8LvfwZtv+iV8TafwjHzhomnIEE0MIXUTpT4XkVQyceLeH/LNmsGtt8KUKbBqFcydC//7v37q1YkT4cgjfa/Seef52YVWrw4kdBERkTqLlv9uvBF+9jM/qcOyZX556CG48EKfE//wBzj2WD9j7NCh8KtfwZNPwrffBtIMSUPqSRJJA+FZfKLN7pOT43uQBg2Ca6+F776Dl17yPUwzZ/qL/wEcemhFL9OQIbrYn4iIpLaa8l9Y9+5+Oe88f//77/15veGepjvvhJtu8o/tv3/lIXq9emXOeU0SP/oXSSRNFBfHPuVp69Zwzjl+2bULFizw04w//zzccINPMK1aVZzLNHJkQkMXERGps9rkv7B99vHD0U8+2d/ftg3mzfMF0xtvwOOP+0mRwJ/be/TRcMwx/uchh+hLRFGRVMnUhVMZ/8p4lm9YTvf87kwcMZHifpqIX9JbTg4MHOiXa66B9et9L9Pzz/tepvA47t69B1FU5IumI45QghBJRcpTInXTpIkfhn7kkXDVVRXXLAz3NL35Jjz2mH9u8+Z7n9eUpTNzZzX9GxQydeFUxs0Yx+Yd/upmyzYsY9yMcQBKQJJRWrWCoiK/OOd7mfzED+X85S9w/fV+DPfxx1f0MnXuHHTUIqI8JRI/OTlw0EF++e//9utWrIC33qoomiZM8HmyQQP/RWO4p+moo3zvk2Q2TdwQMv6V8bsTT9jmHZsZ/8r4gCISSTwzGDAAfvMbuO22+axbB9Onw+jR8Pbb8JOfQJcu/jm//S28/nrV12sSkcRTnhJJrK5d/TD1O+6ADz7w5zU9/7zPkc2bw113wVln+WsU7rcf/Nd/wb/+BZ9+6ospySzqSQpZvmF5rdaLZKL8fJ8AzjrLf+AvXFhxLtNf/+rPZ2rZEo47rmICiC5dgo5aJDsoT4kkV36+H00RPm93+3Z4//2Knqann4bJk/1j7dtXngxiwABo1Ciw0CUOzGVQ6Wtmo4BRXbp0ufjBBx+s1bbnzjmX1dv2niO5Q5MOTBs6rdK6srIyWmTZ4NRsa3O2tRdqbvMPPzTg/ff34Z13WvPuu6359ttcAPbdt4zDD/+OIUO+46CDNtCoUfp8pug4J9bw4cOjXsk8W9U2T0Uer9rkqUyQjX+fYWp7erR91y746qtmLFyYz8KF+SxalM/XXzcFIDe3nL59N9Kv3wb69dvAgQdupFmz8mr3l05tj7eg2l5dnsqoIils8ODBbu7cubXaZs+x3gDNGjVj0qhJe431Li0tpTDyctBZINvanG3thdq12TlYvLiil+nNN/0wvLw8GDGiopepW7fExlxfOs6JZWYqkqKINU9FHq/a5KlMkI1/n2Fqe2HQYdTZ119XnNf0xhv+vN9du/x5TQMGVPQ0HXWUvyBupHRve30E1fbq8pSG24WEE8z4V8azbIO/tPP1I67PyMQjUl9mcPDBfvn1r2HTJnjlFT8BxPPP+wv2gT8hNlwwHX20v9CtiNTNnnmqYU7DjC2QRNJV584VkyMBbNwIc+ZUDNGbNAluu80/1qtX5SF6GdhvkdY0cUOE4n7FLL1iKZ//4nOAvU6QFZGq5eXB6afD3XfD0qW+l+mvf/Unt952m+9datPGP+eee/yV0SNNnQoFBX62oYICf19E9hbOUzefcDM7d+3k6G5HBx2SiFSjZUt/TcL//V949VXYsAHeecdf2LZfP3j2Wbj4YujbF84440jOOMM/9s47/hyoSMqVyaWepCrsu8++HN39aKYsmMLVR12N6TLMIjEzgwMP9Mv//A+UlfnEEB6a99RT/nl9+/oepsaN4fbbYXPoO4lly2Ccn9W41hcPFMkWI3uP5MoXr+SFz19g3KBxQYcjIjFq1AgOP9wvV17pe48+/dT3Mk2fvo5FizrtHo3RtKm/RtPRR/uC6Y47YMsW/5hyZeKpSIpizCFjuHjGxcz9ei6HdTks6HBE0laLFnDqqX5xDpYsqSiY7rxz72/KwBdM//M//qJ/nTpBbm7y4xZJZQe0PYDu+d2Z+dlMFUkiacwM9t/fL716LaGwsBPffOPPa3rjDV88/elP/rymPW3eDFdfDeed53uXJL5UJEVRdGARP3/u59y/4H4VSSJxYgYHHOCXX/4SfvjBD9Wrahz26tWw777+duvWfpx3ly7+Z+QSXtehAzTUJ5pkCTNjZK+RPLzoYXaU76BRA801LJIpOnasuBwH+PN+8/OrzpUrV/ovI/v08bk1XHAdcIC/llNeXnJjzyT6lyKK/Nx8Tj/gdB5e9DA3nXgTjRvojHOReGveHLp33/scJfDXnLjhBj9TUOSyaBF88w2U7zGTqpkvlGoqptq00TdukhlO7H0ik96fxOwVsxnWY1jQ4YhIguTlRc+VrVtDSYkfpTFvnr8gfGSvU+fOexdP++/v96dcWD0VSdUYc8gYHln8CM/95zlOP+D0oMMRyUgTJ/px1Zsj5klp1gxuvjn6OOvycvj2W180rVy5dyG1fLmfTejbb/fetlEjP4Svc2do3PggDjmk6mKqZUtfeImkqhE9R9DAGvDCZy+oSBLJcNFy5e23V86V27bB55/DJ5/4wmnJEn/74Ydh/fqK5+Xm+p6myMIpvKj3yVORVI3jex1Ph+YdmLJgiookkQQJf7iPH++Lm+7dfTKo7kTUBg38cISOHeHQQ6M/b/t23+sUrZj6z3+asWCBn21oT82a7V047VlMderknycShPzcfI7sdiQzP5/JxBETgw5HRBIo1lzZpEnF5EmRnPNfHIaLp/DPDz6Axx5T71NVVCRVo2FOQ4r7FXPHu3ewdvNa2jZrG3RIIhmpuDgxs/M0buw/1Lt3r/rx0tL3KCws5IcfYNWq6MXUu+/6n+FZhSK1alXzEL+OHX0Plki8jew9kvGvjmd12Wo6tOgQdDgikkD1yZVmfhh7+/YwbI+O53DvU2TxtGSJep9UJNVgzIAx3DznZh5Z9AiXHn5p0OGISAI0bw69e/slGud8j1O0QmrlSp9cVq2CnTsrb2sG7drVXEy1a5dd39JJ/Z3Y60TGvzqeFz9/kQsPuTDocEQkDdXU+xRZPH3ySfTepz2LpwMOSO/eJxVJNejfoT+HdDiEKQumqEgSyWJmvteoVau9E0mkXbtg7drqi6m5c2HNmr1nKmrY0Pc61VRMtWql86XEG9hpIO2atWPm5zNVJIlIXEX2Ph1zTOXHInufIouo6nqf9iyiUr33yVxV8wmmKTMbBYzq0qXLxQ8++GDc9vvoV49y1xd3cd/g++jRvAdlZWW0aNEibvtPB9nW5mxrL6jNybZzp/H9941Zu7Yxa9c2Ye3axqxb14R168L3/e1Nm/Yep9e4cTlt226nTZttVf5s23Ybbdpso2nTvS+sEUubX365Pffeuy9r1jShfftt/PSnX3DccWtq3cbhw4fPc84NrvWGGay2eSqW4/Wnj//Eu9+/y+NHPE6OpelXtlXIxs+kMLVdbU9XzsH69Y346qtmLF/ejK++asZXXzVl+fJmrFrVlF27Kr7ha9t2G926baZbt8106LCe3r3L6d59M+3bb62x9ykZeSqjiqSwwYMHu7lz58Ztf9+UfUPXm7ty1VFX8acRf6K0tJTCwsK47T8dZFubs629oDanqi1b/BC+aL1S4Z+RMx6FtWy5dy/UDz/8hx/9qM/udR07+qEWYVOnVj2D0qRJtR8Lb2YqkqKINU/F8h6d+uFULnjiAt67+D0Gd86cX3c6/H0mitpeGHQYgcj0tm/fXvXMe0uWwPffVzwvN7fq6z6Fe5+Slac03C4GHVt05OD2B/OXt/7CDW/eQPsm7bmpzU0U90vAmeYiIhGaNvUX1Q1fWLcqzvmLDVY3xO/11/3tHTv6cOedlbdv27aikHr99b0Lrs2b/YxKiZhcQ+rvhF4nAHDc/cexcdtGuud3Z+KIicpRIpJSGjeGvn39Esk5ePLJt2jb9qhKhdP8+fD445Wvi9i5M6xb54f7RUpEnlKRFIOpC6fy8dqPKXf+KK3etppxM8YBKAmJSODMfK9Ry5b+27Zodu2CGTPeomfPo/YqpsL3f/ih6m2XL09M7FJ/L37xIoaxYZufy37ZhmXKUSKSNsxgn312cMwxe5/7VFXv0333Vb2feOcpFUkxGP/KeLaXb6+0bvOOzYx/ZbwSkIikjZwcyM/fQf/+0L9/1c8pKKj6qu7RplGX4I1/ZTyOykPnlaNEJBNU1fs0a1Zy8lTmnOGZQMs3VF2aRlsvIpKuJk7c+wK5zZr59ZKalKNEJJskK0+pSIpB9/yqS9MGOQ24e+7dbNlRxRUmRUTSUHGxP/m1Rw8/BKJHj7qdDCvJEy1HRVsvIpLOkpWnVCTFYOKIiTRrVLlkbdygMd1aduNnz/6M7rd2Z0LpBNZuXhtQhCIi8VNcDEuX+nOYli5VgZTqqspRzRo1Y+IIdf+JSGZKRp5SkRSD4n7FTBo1iR75PTCMDk068K/T/sXnv/ic10peY2jXoVz32nV0v6U7lz57KZ9/93nQIYuISJYI56iOLToC0LppayaNmqTzkURE6kFFUoyK+xWz9Iql7PrDLqYNnUZxv2LMjGE9hjHjvBks/n+LOe/g87j3g3vpc0cfiv5dxLsr3w06bBERyQLF/Yr5+sqv6dO6D4d0OEQFkohIPalIipMD2x3IP0/7J0svX8rVR13NS5+/xJB7h/Cj+37EjCUz2OV2BR2iiIhkMDPjvIPPo3RpKV9v+jrocERE0pqKpDjrlNeJ64+7nq9++RW3nHgLS9cv5dRpp3Lw3w/mn+//k207t9W8ExERkTo4r995OByPLn406FBERNKaiqQEyWuSxxVDr+Czyz5j6plTadKwCT+d8VMKbivg+jeu5/st3wcdooiIZJgD2h7AwI4DeWjhQ0GHIiKS1lQkJVijBo04v9/5vD/ufV668CX6d+jPNa9eQ7dbunHFzCtYtr6Kq2GJiIjU0XkHn8d7X7/HZ999FnQoIiJpS0VSkpgZx+17HC9c8ALz/3s+Z/Y9k7+99zd63d6L8x87nw9WfRB0iCIikgHOPfhcAKYtmhZwJCIi6cucc0HHEDdmNgoY1aVLl4sffPDBhL1OWVkZLVq0qPd+1mxdw2MrH+OZVc+wuXwzh7Y6lHO6ncNh+xyGmcUh0viJV5vTRba1F9TmbJHMNg8fPnyec25wUl4sTdQ2T9X1eF0+/3I27NjA5MGTUy6fxCob/z7D1Ha1PdsE1fZq85RzLuOWQYMGuUSaNWtWXPe3fst69+c3/+w639TZcR2u39/7uSnzp7htO7fF9XXqI95tTnXZ1l7n1OZskcw2A3NdCuSEVFxizVN1PV5/f/fvjutw81fNr9P2qSAb/z7D1PbspLYnX3V5SsPtUkB+bj5XHXUVX17+Jfeddh8Ox5gnx7Dvbfvy17f/ysZtG4MOUURE0kjRQUU0zGnIw4seDjoUEZG0pCIphTRu0JgxA8bw4SUf8tz5z7Ffm/349Uu/ptst3bjqpatYuXFl0CGKiEgaaNusLcfvezzTFk3TdfpEROpARVIKMjNO6nMSr455lbkXz+XkPidz0+yb6HlbT0qeLGHRmkUATF04lYJbC8iZkEPBrQVMXTg14MhFRCRV9MjvwbINy2j4vw2VI0REaqlh0AFI9QZ1HsTDZz3M9SOu55bZt3DvB/cyZcEU+rfvz5J1S9hW7i9Ou2zDMsbNGAdAcb/iIEMWEZGATV04lSkLpgDgcMoRIiK1pJ6kNFHQqoDbTrqNr375FX8c/kcWfbtod4EUtnnHZsa/Mj6gCEVEJFWMf2U8W3ZuqbROOUJEJHYqktJM66atGT9sPH5Cjr0t37A8yRGJiEiqiZYLlCNERGKjIilNdc/vXqv1IiKSPZQjRETqR0VSmpo4YiLNGjWrtK5RTiMmjpgYUEQiIpIqqsoRjRs0Vo4QEYmRiqQ0VdyvmEmjJtEjvweG0bRhU3a5XQzoMCDo0EREJGB75ojGOY1p2bglZx94dtChiYikBRVJaay4XzFLr1jKrj/s4svLv6R109ac//j5bNu5reaNRUQko0XmiMfOeYy1W9bunvFORESqpyIpQ3Ro0YF/nfYvPlz9Ide+em3Q4YiISAo5pc8pHNb5MP74+h/ZXr496HBERFKeiqQM8uP9fswlgy7hptk38eqXrwYdjoiIpAgzY0LhBJZtWMZ98+8LOhwRkZSnIinD/PWEv9KnTR8ueuIivtvyXdDhiIhIihjZeyRDuw7lj6//UcOyRURqoCIpwzRv3JypZ05l9Q+rueSZS6JeT0lERLJLuDfpq41f8a8P/hV0OCIiKU1FUgYa3HkwEwon8O+P/k27G9uRMyGHglsLmLpwatChiYhIgI7f93j6tO7DZc9fptwgIlKNhkEHIInRrWU3ciyHdVvWAbBswzLGzRgH+BmPREQk+zy06CGWb1hOuSsHlBtERKKxTBqOZWajgFFdunS5+MEHH0zY65SVldGiRYuE7T8ezp1zLqu3rd5rfYcmHZg2dFqt95cObY6nbGsvqM3ZIpltHj58+Dzn3OCkvFiaqG2eivfxinduSKRs/PsMU9vV9mwTVNury1MZVSSFDR482M2dOzdh+y8tLaWwsDBh+4+HnAk5OPY+toax6w+7ar2/dGhzPGVbe0FtzhbJbLOZqUiKItY8Fe/jFe/ckEjZ+PcZprYXBh1GINT2wqS/bnV5SuckZaju+d2rXN+8UXO27NiS5GhERCQVRMsNHVt0THIkIiKpTUVShpo4YiLNGjWrtK5hTkPKdpQx5N4hfLL2E6YunErBrQU6eVdEJEtUlRsMY+O2jUx8faJygohIiCZuyFDhE3DHvzKe5RuW0z2/OxNHTKRN0zZc+MSF9L+rP2a2+8rrOnlXRCTzVZUbfjHkF0x8fSLXzrp29/OUE0Qk26lIymDF/YqrTG4LLlnAvrfty7byyhcT3LxjM+NfGa+EKCKSwarKDTfPvhm2Vn6ecoKIZDMNt8tCnfM67+5B2tPyDcuTHI2IiATt601fV7leOUFEspWKpCwV7eTdaOtFRCRzKSeIiFSmIilLVXXybrNGzZg4YmJAEYmISFCUE0REKlORlKWK+xUzadQkeuT3wDB65Pdg0qhJGnsuIpKFlBNERCrTxA1ZLNrEDiIikn2UE0REKqgnSUREREREJIKKJBERERERkQgqkkRERERERCKoSBIREREREYmgIklERERERCSCiiQREREREZEIKpJEREREREQiqEgSERERERGJYM65oGOIOzP7FliWwJdoC6xN4P5TUba1OdvaC2pztkhmm3s459ol6bXSSi3yVDa+R8PU9uyktmenoNoeNU9lZJGUaGY21zk3OOg4kinb2pxt7QW1OVtkY5vTWTYfL7Vdbc82antqtV3D7URERERERCKoSBIREREREYmgIqluJgUdQACyrc3Z1l5Qm7NFNrY5nWXz8VLbs5Panp1Sru06J0lERERERCSCepJEREREREQiqEiqJTNbamYLzWy+mc0NOp54M7N/mdkaM1sUsa61mb1kZv8J/dwnyBjjLUqbrzOzlaHjPN/MTg4yxngzs25mNsvMPjKzxWZ2eWh9Rh7ratqbscfZzHLN7F0zWxBq84TQ+p5m9o6ZfWZmj5hZ46BjlaqZ2UgzWxI6Vr8JOp54qE2OMe/2UPs/NLNDI7YZE3r+f8xsTBBtqY3afuZmWNtr9VlkZk1C9z8LPV4Qsa/fhtYvMbMTA2pSrZlZAzP7wMyeCd3PirZbFf8zp9V73jmnpRYLsBRoG3QcCWzfMOBQYFHEur8Avwnd/g3w56DjTEKbrwN+FXRsCWxzJ+DQ0O084FPgwEw91tW0N2OPM2BAi9DtRsA7wFDgUeDc0Pq7gZ8FHauWKo9fA+BzYF+gMbAAODDouOLQrphzDHAy8HzovTwUeCe0vjXwRejnPqHb+wTdthraXavP3Axre60+i4D/B9wdun0u8Ejo9oGhv4MmQM/Q30eDoNsX4+/gSuAh4JnQ/axoO1X8z5xO73n1JEklzrnXge/2WH0aMCV0ewpwejJjSrQobc5ozrlVzrn3Q7c3AR8DXcjQY11NezOW88pCdxuFFgccC0wPrc+YY5yBDgc+c8594ZzbDkzD/32mtVrmmNOA+0Pv5TlAKzPrBJwIvOSc+8459z3wEjAy4cHXQx0+czOp7bX9LIr8nUwHRpiZhdZPc85tc859CXyG/ztJaWbWFTgFuDd038iStkeRNu95FUm154AXzWyemY0LOpgk6eCcWxW6/Q3QIchgkujnoS7ff1mGDDurSqg7fyD+272MP9Z7tBcy+DiHhnjMB9bgE8vnwHrn3M7QU1aQ4cViGusCfBVxP5OPVbTPnWi/g7T+3cT4mZtRba/lZ9HuNoYe3wC0IU3bDtwKXAXsCt1vQ/a0var/mdPmPa8iqfaOds4dCpwEXGpmw4IOKJmc7/vMhikR7wJ6AQOAVcBNgUaTIGbWAngMuMI5tzHysUw81lW0N6OPs3Ou3Dk3AOiK/9bxgGAjEqleJn7uRMq2z9ywbP0sMrMfA2ucc/OCjiUg1f7PnOrveRVJteScWxn6uQZ4gvTt7qyN1aEuT0I/1wQcT8I551aHPtR3Af8gA4+zmTXCJ+upzrnHQ6sz9lhX1d5sOM4Azrn1wCzgCPwQhoahh7oCK4OKS6q1EugWcT+Tj1W0z51ov4O0/N3U8jM3o9oeFuNn0e42hh7PB9aRnm0/CjjVzJbih8weC9xGdrQ92v/MafOeV5FUC2bW3MzywreBE4BF1W+VEZ4GwrOJjAGeCjCWpAj/AYecQYYd59AY538CHzvnbo54KCOPdbT2ZvJxNrN2ZtYqdLspcDz+PIhZwOjQ0zLmGGeg94A+oVmwGuNP4n464JgSJdrnztPARaFZr4YCG0LDdF4ATjCzfUJDZE8IrUtZdfjMzaS21/azKPJ3Mhp4NdTj8DRwbmgGuJ5AH+DdpDSijpxzv3XOdXXOFeD/hl91zhWTBW2v5n/m9HnPx3smiExe8LMMLQgti4HxQceUgDY+jB92tAM/7vMn+PGwrwD/AV4GWgcdZxLa/ACwEPgQ/4fbKeg449zmo/Fd3B8C80PLyZl6rKtpb8YeZ6A/8EGobYuA34fW74tPrp8B/waaBB2rlqjH8GT8LGifZ0q+qU2Owc9y9bdQ+xcCgyP281+h9/BnwNig2xVDu2v1mZthba/VZxGQG7r/WejxfSP2NT70O1kCnBR022r5eyikYna7jG87Uf5nTqf3vIVeXERERERERNBwOxERERERkUpUJImIiIiIiERQkSQiIiIiIhJBRZKIiIiIiEgEFUkiIiIiIiIRVCSJiIiIJICZOTO7KeL+r8zsujjt+z4zG13zM2PeX76Z3W9mn5nZ56Hb+RGP32hmi83sxiq2HWlm75rZJ2Y238weMbPuUV7nEjO7qIr1BWa217XqQuu3hPb7kZndbWZR/3+Ntv89njPAzE6u7jkiKpIkpZhZRzObFvqAnmdmz5nZfgl+zVIzG1zDc64ws2YR958LXxyvnq+91MwWmtmHZvaamfWox74SEmMVr9PJzJ4J3W5mZlNDbVhkZm+aWYt67r+dmc2MT7QiIoHaBpxpZm2DDiSSmTWsYvU/gS+cc72dc72AL4F7Ix4fB/R3zv16j30dDNwBjHHOHeCcGwBMBQqqel3n3N3OuftrGfLnof32Bw4ETo/2xBj3PwB/nSqRqFQkScoIXZH8CaDUOdfLOTcI+C3QIdjIALgC2F2AOOdOds6tj9O+hzvn+gOlwLX12M8VJC7GSFcC/wjdvhxY7Zzr55w7GH9hyB312blz7ltglZkdVb8wRUQCtxOYBPxyzwf27Akys7LQz8LQl2ZPmdkXZnaDmRWHemoWmlmviN0cZ2ZzzexTM/txaPsGoV6f90JfwP13xH7fMLOngY/2iKU3MAj4v4jV/wsMNrNeoW1aAPPM7Jw9mnI18Cfn3MfhFc65p51zr4f2XWpmt5rZXOByM7vOzH4VemyQmS0wswXApTX9Mp1zO4G3gd6hHqZXQ218Jdxztcf+S83sz6Hf3admdoyZNQ617ZxQ79Q5Zvaj0O35ZvaBmeXVFItkPhVJkkqGAzucc3eHVzjnFjjn3gh9uD8TXm9md5pZSej2UjO7PvThNtfMDjWzF0K9UZeEnhN1+0hmdldoH4vNbEJo3S+AzsAsM5sV8ZptQ8nr0ojtIz+cfx2RpCbE0P7ZQJfQttUlz1Izm25+WMNU86qLsSD03PtCSWKqmR1nZm+Z2X/M7PDQ85ub2b9CyeQDMzstSpxnAeGenk7AyvADzrklzrltof1dENrXfDO7x8wahNaPNLP3Q4nxlSiv8SRQHMPvTEQk1f0NKLaIoWsxOAS4BOgLXAjs55w7HN+zc1nE8wqAw4FTgLvNLBf/ZdUG59xhwGHAxWbWM/T8Q4HLnXN7jtA4EJjvnCsPrwjdng8c5Jw7FdjinBvgnHtkj20PAt6voT2NnXODnXM37bF+MnCZc+6QGrYH/OgFYASwEN97NSX0JeNU4PYomzUM/e6uAP7gnNsO/B54JKI9vwIuDfVWHQNsiSUeyWwqkiSVHAzMq+O2y0Mfbm8A9wGjgaFALMVJpPHOucH4Lv0fmVl/59ztwNf4Hp/hezz/EeDsiPtnA4+Y2QlAH3zyGgAMMrNhNbz2SHxxUJOB+A/7A4F9gaNqiBGgN3ATcEBoOR84Gp8Yrgk9ZzzwaiiZDAduNLPmkTsJJdrvw4UQ8C/gajObbWZ/NLM+oef1Bc4JxTYAKMf/k9AO3wt1VigpFkVp41x8ohIRSWvOuY3A/cAvarHZe865VaHP2s+BF0PrF1J5GNujzrldzrn/AF/gP99PAC4ys/nAO0AbfD4CeNc592Vd21ITM2sT+mLs0/AXhiF7FlaYHw7eKtzjBDxQza57hdrzFvCsc+554AjgoYhtj46y7eOhn/OoYghgyFvAzaEvHFuFeqwky1U1JlUkHT0d+rkQaOGc2wRsMrNtVrvzcs42s3H4v41O+ELkw2hPds59YGbtzawz0A5fQHxlZpfjE9UHoae2wCep16vYzSwzaw2UAb+LIcZ3nXMrAEJJowB4s4ZtvnTOLQxtsxh4xTnnzCwy4Z4AnBqR2HKB7sDHEfvpBHwbvuOcm29m+4a2PQ54z8yOwH/TNyh0H6ApsAZfuL4eTtLOue+ixLsG3zMmIpIJbsX3tkyOWLeT0JfV5iciaBzx2LaI27si7u+i8v9ubo/XcYDhe2deiHzAzAqBH6LE9xEwwMxynHO7ImIawB5D86qwGN9DtcA5ty60n1/h815YtNeNVficpLoI/+7KifJ/r3PuBjN7Fn+e0ltmdqJz7pM6vp5kCPUkSSpZjP/Huiq7k0lI7h6PRyaQPZNLwxi2D/eS/AoYEeq+f7aq51Xh3/ieq3Oo+LbMgOtDXfkDQifC/jPK9sOBHvhhDeGer1iTZ9QP/T3EknAN38MTjrl75BjzkC3s8TtxzpU55x53zv0/4EF8kjH8MIjwvvZ3zl0XQ5xhuWi4g4hkiNAXQo/ih8KFLaUi550KNKrDrovMLMf8eUr7AkuAF4CfmVkjADPbb89RAVXE9xn+S73I82KvBd4PPVadvwDjQyMIwppFe3LEa64H1ptZuAeotkOs3wbOjdj2jVpsuwnYfd6RmfVyzi10zv0ZeA/fIydZTkWSpJJXgSahnhwAzKy/mR0DLAMONLMmoZ6hEbXcdyzbt8R/27XBzDoAJ0U8VukDdQ+P4D+oR+MLJvBJ6r8sNNObmXUxs/bRggt17V+BHyLRmrolz+pijMULwGUW6voxs4FVPOdTIoYrmNlRZrZP6HZjfM/bMuAVYHS4zWbW2vzMfXOAYeHx8aG2VmU/YK+pYEVE0thNQOQsd//AD+tegB86VpfeluXAu8DzwCXOua3485Y+At43P6X2PcT2ZdpPgP3Mn8/7Of5z+Cc1bENolMLlwP1mtsTM3sKfS/VQ9VsCMBb4W2hUhMXw/EiXAWPN7EP8eVuX12LbWfj/Ceabn4jiCvMztH6In3zo+VrGIhlIw+0kZYSGf50B3GpmVwNb8cXCFaEhbI/i/3H+kophbLHuu8btnXMLzOwD4BPgK/wY5bBJwEwz+3rPc36cc4vNz4Sz0jm3KrTuxdC3arNDNUcZcAF+GFm0GFeZ2cP4GX4mAU+FkudMYkueUWOM0f/hh4R8GOq9+hL48R4x/hBKoL1D3y72Au4KFVY5+N63x0LH8lrgxdC+duBPip0TKoIfD61fAxxvfgr2S5xzPw291PDQvkRE0pZzrkXE7dVUnoF0NX4IctjVofWl+NlOw88rjLi9+zHnXEmU19yFP9f0mj0eqrTfKrb7Hp+namxLFY89S5TP7Mj4Q/evi7g9Dz9JRdhVVWy/FH/O8p7rlwHHVrE+cv+FEbfXEvqSL9Szd1jEZnudMyVizu05nFVEJLpQITvIOVef6cpreo3XgdNCSVtEREQkqdSTJCK14px7wszaJGr/oRnwblaBJCIiIkFRT5KIiIiIiEgETdwgIiIiIiISQUWSiIiIiIhIBBVJIiIiIiIiEVQkiYiIiIiIRFCRJCIiIiIiEuH/Ax9NdknyZcp0AAAAAElFTkSuQmCC", 330 | "text/plain": [ 331 | "
" 332 | ] 333 | }, 334 | "metadata": { 335 | "needs_background": "light" 336 | }, 337 | "output_type": "display_data" 338 | } 339 | ], 340 | "source": [ 341 | "fig = plt.figure(figsize=(14,5))\n", 342 | "ax = fig.add_subplot(1,2,1)\n", 343 | "ax.plot(np.cumsum(t_total_sg) ,error_l2_mean_sg, '-o', color='blue',label=\"SG\")\n", 344 | "ax.plot(np.cumsum(t_total_ddsg),error_l2_mean_ddsg,'-o', color='green',label=\"DDSG\")\n", 345 | "ax.set_yscale('log')\n", 346 | "ax.set_ylabel('Stagnation (L2-Norm)')\n", 347 | "ax.set_xlabel('Cumulative Runtime (Sec.)')\n", 348 | "ax.grid(visible=True, which='both',axis='both')\n", 349 | "ax.legend()\n", 350 | "\n", 351 | "ax = fig.add_subplot(1,2,2)\n", 352 | "ax.plot(np.cumsum(grid_points_sg) ,error_l2_mean_sg, '-o', color='blue',label=\"SG\")\n", 353 | "ax.plot(np.cumsum(grid_points_ddsg) ,error_l2_mean_ddsg,'-o', color='green',label=\"DDSG\")\n", 354 | "ax.set_yscale('log')\n", 355 | "ax.set_ylabel('Stagnation (L2-Norm)')\n", 356 | "ax.set_xlabel('Number Of Grid Points')\n", 357 | "ax.grid(visible=True, which='both',axis='both')\n", 358 | "ax.legend()\n", 359 | "\n", 360 | "fig.show()" 361 | ] 362 | }, 363 | { 364 | "cell_type": "markdown", 365 | "metadata": {}, 366 | "source": [ 367 | "The errors reported here indicate how far the current policy and the policy for the next period are from optimal when calculated using the computed equilibrium policy function. Along the N-step simulation path, the errors are computed. We report the _average_ and _maximum_ (99.9%ile) simulation errors in log base 10. We can see that DDSG and SG have similar errors, but DDSG takes much less time and uses far fewer grid points." 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": 16, 373 | "metadata": {}, 374 | "outputs": [ 375 | { 376 | "name": "stdout", 377 | "output_type": "stream", 378 | "text": [ 379 | " Sim. Avg. Euler Error (log10) Sim. Max. Euler Error (log10) Cumulitive Runtime (Sec.) Cumulitive Number of Grid Points\n", 380 | "---- ------------------------------- ------------------------------- --------------------------- ----------------------------------\n", 381 | "SG -1.751 -1.45376 8.22627 1059\n", 382 | "DDSG -1.77295 -1.48821 9.26019 457\n" 383 | ] 384 | } 385 | ], 386 | "source": [ 387 | "\n", 388 | "# Number of simulation steps\n", 389 | "N=11000\n", 390 | "\n", 391 | "# evaluation simulation erro\n", 392 | "def error_sim_eval(folder_name):\n", 393 | " p = DDSG(folder_name)\n", 394 | " error = model.error_sim(policy_funcion=p,N=N)\n", 395 | " error = error[int(N*.1):,:] # Remove the frist N_ignore (burnin)\n", 396 | " error_econ_abs_mean=np.mean(np.abs(error.flatten()))\n", 397 | " error_econ_abs_max999=np.percentile(np.abs(error.flatten()),99.9)\n", 398 | "\n", 399 | " return [error_econ_abs_mean,error_econ_abs_max999]\n", 400 | "\n", 401 | "data=[]\n", 402 | "\n", 403 | "error = error_sim_eval('p_sg')\n", 404 | "data.append(['SG',np.log10(error[0]),np.log10(error[1]),np.sum(t_total_sg[-1]),np.sum(grid_points_sg[-1])])\n", 405 | "\n", 406 | "error = error_sim_eval('p_ddsg')\n", 407 | "data.append(['DDSG',np.log10(error[0]),np.log10(error[1]),np.sum(t_total_ddsg),np.sum(grid_points_ddsg)])\n", 408 | "\n", 409 | "headers = ['Sim. Avg. Euler Error (log10)','Sim. Max. Euler Error (log10)','Cumulitive Runtime (Sec.)','Cumulitive Number of Grid Points']\n", 410 | "\n", 411 | "print(tabulate(data,headers=headers))\n" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "metadata": {}, 417 | "source": [ 418 | "## 4 Strong-Scaling\n", 419 | "\n", 420 | "To run DDSG in parallel, the script must be executed using `mpirun`; for example, to run the script with four processes, use the command `mpirun -—bind-to core -np 4 python3 script.py`. The `-—bind-to core` flag ensures that the processes are linked to the machine's cores. \n", 421 | "\n", 422 | "Here we show scalablity of the DDSG time-iteation alogirthem. Unlike the previous examples, we use the _smooth_ IRBC model as the test case. We only take one iteartion for each test. In all test we fix maximimum SG refinment to level 8 and maximum expansion order to 1. We use two time-iteartion steps and report the cumulitive the number of grid-points and cumulitie runtime." 423 | ] 424 | }, 425 | { 426 | "cell_type": "code", 427 | "execution_count": 17, 428 | "metadata": {}, 429 | "outputs": [ 430 | { 431 | "data": { 432 | "image/png": "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", 433 | "text/plain": [ 434 | "
" 435 | ] 436 | }, 437 | "metadata": { 438 | "needs_background": "light" 439 | }, 440 | "output_type": "display_data" 441 | } 442 | ], 443 | "source": [ 444 | "num_country_sg = [2,3,4]\n", 445 | "time_sg = [1.16e+00,1.24e+01,1.28e+02]\n", 446 | "\n", 447 | "#mpirun --bind-to core -np 1 python3 unit_test_IRBC.py 2 4 0 2\n", 448 | "#mpirun --bind-to core -np 1 python3 unit_test_IRBC.py 3 4 0 2\n", 449 | "#mpirun --bind-to core -np 1 python3 unit_test_IRBC.py 4 4 0 2\n", 450 | "\n", 451 | "num_country_ddsg = [2,3,4,8,16]\n", 452 | "time_ddsg = [3.71e-01,7.93e-01,1.53e+00,8.70e+00,8.79e+01]\n", 453 | "#mpirun --bind-to core -np 1 python3 unit_test_IRBC.py 2 4 1 2\n", 454 | "#mpirun --bind-to core -np 1 python3 unit_test_IRBC.py 3 4 1 2\n", 455 | "#mpirun --bind-to core -np 1 python3 unit_test_IRBC.py 4 4 1 2\n", 456 | "#mpirun --bind-to core -np 1 python3 unit_test_IRBC.py 8 4 1 2\n", 457 | "#mpirun --bind-to core -np 1 python3 unit_test_IRBC.py 16 4 1 2\n", 458 | "\n", 459 | "n_proc = [1,2,4,8,16]\n", 460 | "#mpirun --bind-to core -np 1 python3 unit_test_IRBC.py 16 4 1 2\n", 461 | "#mpirun --bind-to core -np 2 python3 unit_test_IRBC.py 16 4 1 2\n", 462 | "#mpirun --bind-to core -np 4 python3 unit_test_IRBC.py 16 4 1 2\n", 463 | "#mpirun --bind-to core -np 8 python3 unit_test_IRBC.py 16 4 1 2\n", 464 | "#mpirun --bind-to core -np 16 python3 unit_test_IRBC.py 16 4 1 2\n", 465 | "time_ddsg_16 = [8.79e+01,4.51e+01,2.52e+01,1.39e+01,8.64e+00]\n", 466 | "\n", 467 | "#mpirun --bind-to core -np 1 python3 unit_test_IRBC.py 8 4 1 2\n", 468 | "#mpirun --bind-to core -np 2 python3 unit_test_IRBC.py 8 4 1 2\n", 469 | "#mpirun --bind-to core -np 4 python3 unit_test_IRBC.py 8 4 1 2\n", 470 | "#mpirun --bind-to core -np 8 python3 unit_test_IRBC.py 8 4 1 2\n", 471 | "#mpirun --bind-to core -np 16 python3 unit_test_IRBC.py 8 4 1 2\n", 472 | "time_ddsg_8 = [8.22e+00,4.54e+00,2.76e+00,1.66e+00,1.01e+00]\n", 473 | "\n", 474 | "# Plot\n", 475 | "fig = plt.figure(figsize=(14,5))\n", 476 | "ax = fig.add_subplot(1,2,1)\n", 477 | "ax.plot(num_country_sg,time_sg,'-o', color='blue')\n", 478 | "ax.plot(num_country_ddsg,time_ddsg,'-o', color='green')\n", 479 | "ax.set_ylabel('Run-Time (per iteration)')\n", 480 | "ax.set_xlabel('Number of Countries')\n", 481 | "ax.set_xticks(n_proc)\n", 482 | "ax.set_yscale('log')\n", 483 | "ax.set_xscale('log',base=2)\n", 484 | "ax.grid(visible=True, which='both',axis='both')\n", 485 | "ax.legend(['$SG$','$DDSG$'])\n", 486 | "\n", 487 | "ax = fig.add_subplot(1,2,2)\n", 488 | "ax.plot(n_proc,time_ddsg_16,'-o', color='blue')\n", 489 | "ax.plot(n_proc,time_ddsg_8,'-o', color='red')\n", 490 | "ax.set_ylabel('Run-Time (per iteration)')\n", 491 | "ax.set_xlabel('Number of Processes')\n", 492 | "ax.set_xticks(n_proc)\n", 493 | "ax.set_yscale('log')\n", 494 | "ax.set_xscale('log',base=2)\n", 495 | "ax.legend(['$num\\_countries=16$','$num\\_countries=8$'])\n", 496 | "ax.grid(visible=True, which='both',axis='both')\n", 497 | "plt.show()" 498 | ] 499 | } 500 | ], 501 | "metadata": { 502 | "interpreter": { 503 | "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" 504 | }, 505 | "kernelspec": { 506 | "display_name": "Python 3.8.10 64-bit", 507 | "language": "python", 508 | "name": "python3" 509 | }, 510 | "language_info": { 511 | "codemirror_mode": { 512 | "name": "ipython", 513 | "version": 3 514 | }, 515 | "file_extension": ".py", 516 | "mimetype": "text/x-python", 517 | "name": "python", 518 | "nbconvert_exporter": "python", 519 | "pygments_lexer": "ipython3", 520 | "version": "3.8.10" 521 | }, 522 | "orig_nbformat": 4, 523 | "vscode": { 524 | "interpreter": { 525 | "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" 526 | } 527 | } 528 | }, 529 | "nbformat": 4, 530 | "nbformat_minor": 2 531 | } 532 | -------------------------------------------------------------------------------- /examples/irbc/unit_test.py: -------------------------------------------------------------------------------- 1 | # add root into path 2 | import os 3 | import sys 4 | 5 | import time 6 | import numpy as np 7 | from tabulate import tabulate 8 | from scipy import optimize 9 | from IRBC import IRBC 10 | from DDSG import DDSG 11 | 12 | #input parameters 13 | num_countries = int(sys.argv[1]) 14 | l_max = int(sys.argv[2]) 15 | k_max = int(sys.argv[3]) 16 | iter_max = int(sys.argv[4]) 17 | 18 | # IRBC model 19 | model = IRBC(num_countries=num_countries, irbc_type='non-smooth') 20 | model.set_parameters() 21 | model.set_integral_rule() 22 | 23 | def eq_condition(X): 24 | global p_last 25 | [n,d]=X.shape 26 | result = np.empty(shape=(n,model.grid_dof)) 27 | for i in range(0,n): 28 | state = X[i,:] 29 | p_guess = p_last.eval(X[i,:].reshape(1,-1)) 30 | solution = optimize.root(fun=model.system_of_equations, x0=p_guess,tol=1e-10,args=(state,p_last), method='hybr') 31 | result[i,:] = solution.x 32 | return result 33 | 34 | def eq_condition_init_guess(X): 35 | [n,d]=X.shape 36 | val = np.empty(shape=(n,model.grid_dof)) 37 | for i in range(0,n): 38 | val[i,0:model.num_countries] = (model.k_min + model.k_max)/2 39 | val[i,model.num_countries] = 1 40 | val[i,model.num_countries+1:] = -0.1 41 | 42 | return val 43 | 44 | # main parameters 45 | eps_sg = 1e-3 46 | l_max = l_max 47 | iter_max = iter_max 48 | 49 | #domain of the grid 50 | domain = np.zeros((model.grid_dim,2)) 51 | domain[0:model.num_countries,0] = model.k_min 52 | domain[0:model.num_countries,1] = model.k_max 53 | domain[model.num_countries:,0] = model.a_min 54 | domain[model.num_countries:,1] = model.a_max 55 | 56 | # hdmr anchor point ... is ignored if SG is used 57 | x0=np.mean(domain,axis=1).reshape((1,domain.shape[0])) 58 | 59 | # sample points for policy convergence/stagnation 60 | X_sample = np.random.uniform(low=domain[:,0],high=domain[:,1],size=(1000,model.grid_dim)) 61 | 62 | # initial policy 'guessed' funciton ... corresponding to the "eq_condition_init_guess" 63 | p_last = DDSG() 64 | p_last.init(f_orical=eq_condition_init_guess,d=model.grid_dim,m=model.grid_dof) 65 | p_last.set_grid(domain=domain,l_max=l_max,eps_sg=eps_sg) 66 | 67 | # if k_max is less than 1, we use SG 68 | if k_max>0: 69 | p_last.set_decomposition(x0,k_max=k_max,eps_rho=1e-6,eps_eta=1e-6) 70 | 71 | p_last.sg_prl=True 72 | p_last.build(verbose=1) 73 | 74 | if p_last.proc_rank==0: 75 | model.print_parameters() 76 | 77 | t_total =[] 78 | error_l2_mean =[] 79 | grid_points =[] 80 | 81 | # time-iteration 82 | for i in range(0,iter_max): 83 | 84 | t_total.append(-time.time()) 85 | 86 | # construct new policy, i.e., p_next, using p_last 87 | p_next = DDSG() 88 | p_next.init(eq_condition,d=model.grid_dim,m=model.grid_dof) 89 | p_next.set_grid(domain=domain,l_max=l_max,eps_sg=eps_sg) 90 | if k_max>0: 91 | p_next.set_decomposition(x0,k_max=k_max,eps_rho=1e-6,eps_eta=1e-6) 92 | 93 | p_next.sg_prl=True 94 | p_next.build(verbose=0) 95 | 96 | t_total[-1] += time.time() 97 | 98 | # evaluate the difference two incremental policies ... a measure of stagnation 99 | diff = p_next.eval(X_sample) - p_last.eval(X_sample) 100 | error_l2_mean.append(np.linalg.norm(diff.flatten())/diff.size) 101 | grid_points.append(p_next.num_grid_points) 102 | 103 | if p_next.proc_rank==0: 104 | print('# iter:{:d} time(Sec):{:.2e} error_l2:{:.2e} gridpoints:{:.2e}'.format(i,t_total[-1],error_l2_mean[-1],grid_points[-1]) ) 105 | 106 | # swap policy 107 | p_last = p_next 108 | 109 | data=[] 110 | headers = ['Cumulitive Rutme Time (Sec.)','Cumulitive Number of Grid Points'] 111 | data.append(['Result',np.sum(t_total),np.sum(grid_points)]) 112 | 113 | if p_next.proc_rank==0: 114 | print(tabulate(data,headers=headers)) 115 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mpi4py 2 | numpy 3 | scipy 4 | matplotlib 5 | tabulate 6 | Tasmanian 7 | dill 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, dist 2 | 3 | with open("README.md", "r") as rd: 4 | long_description = rd.read() 5 | 6 | setup( 7 | name='HDMR', 8 | version='0.0.1', 9 | packages=['DDSG', 'IRBC'], 10 | description='High-Dimensional Dynamic Stochastic Model Representation', 11 | url='https://github.com/SparseGridsForDynamicEcon/HDMR', 12 | author='Aryan Eftekhari, Simon Scheidegger', 13 | author_email='aryan.eftekhari@unil.ch, simon.scheidegger@unil.ch', 14 | license='MIT', 15 | install_requires=['mpi4py', 16 | 'numpy', 17 | 'scipy', 18 | 'matplotlib', 19 | 'tabulate', 20 | 'Tasmanian', 21 | 'dill', 22 | ], 23 | classifiers=[ 24 | 'Development Status :: 5 - Production/Stable', 25 | 'Intended Audience :: Science/Research', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Programming Language :: Python :: 3', 28 | ], 29 | ) 30 | --------------------------------------------------------------------------------