Source code for snowdrop.src.numeric.optimization.optimize

# -*- coding: utf-8 -*-
"""
Generic optimization module.

@author: A.Goumilevski
"""
import os
from time import time
import numpy as np
from warnings import filterwarnings
#from numba import njit
from scipy.optimize import minimize, root, Bounds
from scipy.optimize import LinearConstraint
from scipy.optimize import NonlinearConstraint
from dataclasses import dataclass
from snowdrop.src.misc.termcolor import cprint
from snowdrop.src.model.util import importModel
#from snowdrop.src.model.util import loadLibrary
from snowdrop.src.model.util import getLimits, getConstraints
#from snowdrop.src.model.util import print_path_solution_status
from snowdrop.src.graphs.util import bar_plot
from snowdrop.src.preprocessor.function import get_function_and_jacobian as fun
from snowdrop.src.utils.prettyTable import PrettyTable

fpath = os.path.dirname(os.path.abspath(__file__))
it = 0

[docs] @dataclass class Data: success: bool; x: float; fun: float; nfev: int = 0; message: str = ""
#@njit
[docs] def solver(model): """ Find solution of linear constraint optimization problem. Parameters: :param model: Model object. :type model: Model. :returns: solution of optimization problem. """ filterwarnings("ignore") MAXITER = 1000 solver = model.symbolic.SOLVER method = model.symbolic.METHOD if method is None: sign = 1 else: method = method.lower() if method == "minimize": sign = 1 elif method == "maximize": sign = -1 else: cprint("\nPlease choose either Maximize or Minimize method.\n","red") raise ValueError(f"Method {method} is not implemented.") constraints = model.symbolic.constraints obj_func = model.symbolic.objective_function var_names = model.symbols['variables'] var_values = model.calibration['variables'] #var = dict(zip(var_names,var_values)) par_names = model.symbols['parameters'] par_values = model.calibration['parameters'] cal = dict(zip(par_names,par_values)) n = len(var_names) #n_par = len(par_names) eqs_labels = model.eqLabels # Assign missing parameters to zero. for k in cal: val = cal[k] if np.isnan(val): cal[k] = 0 x0 = np.copy(var_values) ### Objective function def mcp_fobj(x): global it it += 1 f = func(x) return sign*np.sum(f**2) ### Objective function def fobj(x): global it it += 1 loc = cal.copy() for i,v in enumerate(var_names): loc[v] = x[i] f = sign*eval(obj_func,{},loc) return f # Function and Jacobian def func_jac(x): global it it += 1 I = np.eye(n) f ,jacob = fun(model=model,y=np.vstack((x,x,x)),params=par_values,order=1) jac = jacob[:,n:2*n] if bool(Il) and bool(Iu): a1 = np.array([upper[i]-x[i] if not np.isinf(upper[i]) else -x[i] for i in range(n)]) b1 = -f b = map_func(a1,b1) a = np.array([x[i]-lower[i] if not np.isinf(lower[i]) else x[i] for i in range(n)]) z = map_func_der(a1,b1)*I + map_func_der(b1,a1)*jac y = map_func_der(a,b)*I - map_func_der(b,a)*z elif bool(Il): a = np.array([x[i]-lower[i] if not np.isinf(lower[i]) else x[i] for i in range(n)]) b = f y = map_func_der(a,b)*I + map_func_der(b,a)*jac elif bool(Iu): a = np.array([upper[i]-x[i] if not np.isinf(upper[i]) else -x[i] for i in range(n)]) b = -f y = map_func_der(a,b)*I + map_func_der(b,a)*jac else: y = -jac return f,y # Function def func(x): f = fun(model=model,y=np.vstack((x,x,x)),params=par_values,order=0) return f # Mapping function def map_func(a,b): return np.sqrt(a*a+b*b)-a-b # Mapping function derivatives def map_func_der(a,b): return a/np.sqrt(a*a+b*b)-1 # MCP Squared Function def path_scalar_func(x): f = path_func(x) f2 = np.sum(f*f) if np.isnan(f2): return 1.e10 else: return f2 # MCP Function def path_func(x): f = func(x) if bool(Il) and bool(Iu): a1 = np.array([upper[i]-x[i] if not np.isinf(upper[i]) else -x[i] for i in range(n)]) b1 = -f b = map_func(a1,b1) a = np.array([x[i]-lower[i] if not np.isinf(lower[i]) else x[i] for i in range(n)]) y = map_func(a,b) elif bool(Il): a = np.array([x[i]-lower[i] if not np.isinf(lower[i]) else x[i] for i in range(n)]) b = f y = map_func(a,b) elif bool(Iu): a = np.array([upper[i]-x[i] if not np.isinf(upper[i]) else -x[i] for i in range(n)]) b = -f y = -map_func(a,b) else: y = -f return y # MCP Jacobian def path_jacob(x): f,jac = func_jac(x) return jac # Jacobian matrix def jacob(x): f,jac = FuncJacob(x) return jac # Jacobian matrix def FuncJacob(x): f,jac = fun(model=model,y=np.vstack((x,x,x)),params=par_values,order=1) return f,jac[:,n:2*n] # Hessian matrix def hess(x,v): _,_,hessian = fun(model=model,y=np.vstack((x,x,x)),params=par_values,order=2) #print(hessian) y = 0 for i in range(len(v)): y += v[i] * hessian[i][n:2*n,n:2*n] return y # Get variables constraints Il,Iu,lower,upper = getLimits(var_names,constraints,cal) for i in range(n): x0[i] = min(upper[i],max(lower[i],x0[i])) # Get bounds bounds = Bounds(lower,upper) if not method is None and (method.lower() == "minimize" or method.lower() == "maximize"): if model.isLinear: #Get jacobian. It is constant for linear problems. jacobian = jacob(x=x0) # Build linear constraints matrix A,lb,ub = getConstraints(n,constraints,cal,eqs_labels,jacobian) constraint = LinearConstraint(A,lb,ub) else: ind = [True]*n for i in range(n): if upper[i] == np.inf and lower[i] == -np.inf: ind[i] = False Lower = lower[ind]; Upper = upper[ind] if model.order == 2: constraint = NonlinearConstraint(func,Lower,Upper,jac=jacob,hess=hess) elif model.order == 1: constraint = NonlinearConstraint(func,Lower,Upper,jac=jacob) elif model.order == 0: constraint = NonlinearConstraint(func,Lower,Upper) if solver in ['trust-constr','SLSQP']: results = minimize(fun=fobj,x0=x0,method=solver,bounds=bounds,constraints=constraint,tol=1.e-10,options={'disp':False,'maxiter':MAXITER}) else: cprint("\nOnly 'trust-constr' and 'SLSQP' methods are implemented...","red") cprint("'trust-constr' method.","red") results = minimize(fun=fobj,x0=x0,method="trust-constr",bounds=bounds,constraints=constraint,tol=1.e-10,options={'disp':False,'maxiter':MAXITER}) if not results.success: cprint("\nConstrained minimization failed.","red") cprint("Running un-constrained minimization...","red") results = minimize(fun=fobj,x0=x0,method='Powell',bounds=None,options={'disp':False,'maxiter':1000}) else: if solver in ['trust-constr','SLSQP']: results = minimize(fun=fobj,x0=x0,method=solver,bounds=bounds,constraints=constraint,tol=1.e-10,options={'disp':False,'maxiter':MAXITER}) elif solver in ['MCP','CONSTRAINED_OPTIMIZATION','PATH','ROOT']: cprint(f"\n{solver} solver","green") # if solver == 'PATH': # x = x0 # f = np.zeros(n) # os.environ['PATH_LICENSE_STRING'] = '2830898829&Courtesy&&&USR&45321&5_1_2021&1000&PATH&GEN&31_12_2025&0_0_0&6000&0_0' # # Load path library # libc = loadLibrary() # # Call path solver C++ function # status = libc.path_solver(n,n_par,x,par_values,f,lower,upper) # print_path_solution_status(status) # results = Data(status,x,np.sqrt(np.sum(f*f)),"Success") if solver == 'CONSTRAINED_OPTIMIZATION': results = minimize(fun=path_scalar_func,x0=x0,method="trust-constr",bounds=bounds,tol=1.e-8,options={'disp':False,'maxiter':MAXITER}) elif solver == 'MCP': from compecon import MCP try: F = MCP(f=func_jac,a=lower,b=upper,x0=x0,maxit=1500) x = F.zero(transform='minmax') f = func_jac(x)[0] results = Data(success=True,x=x,fun=np.sqrt(np.sum(f*f)),nfev=it) except np.linalg.LinAlgError: cprint("\nCompecon MCP solver failed.","red") cprint("Running constrained minimization solver...\n","red") results = minimize(fun=path_scalar_func,x0=x0,method="trust-constr",bounds=bounds,tol=1.e-6,options={'disp':False,'maxiter':MAXITER}) elif solver == 'ROOT': results = root(fun=path_func,jac=path_jacob,x0=x0,method="lm",tol=1.e-8,options={'disp':False,'maxiter':MAXITER}) else: import sys cprint(f"\n{solver} is not implemented. Exitting...\n","red") sys.exit() else: cprint("\nOnly 'MCP', 'CONSTRAINED_OPTIMIZATION', 'ROOT' and 'PATH' solvers are implemented...","red") cprint("'CONSTRAINED_OPTIMIZATION' solver.","red") results = minimize(fun=path_scalar_func,x0=x0,method="trust-constr",bounds=bounds,tol=1.e-8,options={'disp':False,'maxiter':MAXITER}) if not results.success: cprint("\nConstrained solver failed.","red") cprint("Running un-constrained solver...\n","red") results = minimize(fun=path_scalar_func,x0=x0,method="trust-constr",tol=1.e-10,options={'disp':False,'maxiter':MAXITER}) return results
[docs] def run(fpath=None,fout=None,Output=False,plot_variables=None,model_info=False): """ Call main driver program. Runs model simulations. Parameters: :param fpath: Path to model file. :type fpath: str. :param fout: Path to output excel file. :type fout: str. :param Output: If True save results in excel file. :type Output: bool. :param plot_variables: Plot variables. :type plot_variables: list. :param model_info: If True creates a pdf/latex model file. :type model_info: bool. :returns: Optimization results. """ global it model = importModel(fpath) if Output: print(model) var_names = model.symbols['variables'] var_values = model.calibration['variables'] par_names = model.symbols['parameters'] par_values = model.calibration['parameters'] # par = dict(zip(par_names,par_values)) # co2_parameters = [x for x in par_names if x.startswith("PCARB")] shock_names = []; shock_values = [] T = 1 if model.symbolic.SOLVER == "PATH": from snowdrop.src.utils.util import create_config_file create_config_file(T,var_names,var_values,shock_names,shock_values,par_names,par_values,model.options) t0 = time() # Run baseline scenario results = solver(model=model) y = results.x n = len(y) var = dict(zip(var_names,y)) func = results.fun nfev = results.nfev status = "success" if results.success else "failure" elapsed = time() - t0 model.calibration['variables'] = y if bool(model.symbolic.objective_function): #cprint(f"\nObjective function:\n {model.symbolic.objective_function}","blue") cprint("\nObjective function value: {:.2e}".format(func),"green") cprint(f"Solution status: {status}","green") cprint(f"Number of function calls: {nfev}","green") cprint("Elapsed time: {:.2f} (seconds) \n".format(elapsed),"green") # Plot bar graphs # if bool(co2_parameters): # # Set CO2 price to zero # cal = model.calibration # for p in co2_parameters: # cal[p] = 0 # model.calibration = cal # # Run scenrio # results = solver(model=model) # var2 = dict(zip(var_names,model.calibration['parameters'])) # arr = [np.array([var[x],var2[x]]) for x in var_names] # var2 = dict(zip(var_names,arr))plot(var=var,var_names=var_names,par=par,par_names=par_names,fig_sizes=(8,6),title="Equivalent Variation") # plotBar(var=var2,var_names=var_names,par=par,par_names=par_names,yLabel="percent",fig_sizes=(8,6),title="Relative Impact on Welfare",relative=True) # plot(var=var,var_names=var_names,par=par,par_names=par_names,fig_sizes=(8,6),title="Equivalent Variation") if not plot_variables is None: bar_plot(var=var,var_names=plot_variables,plot_variables=True,symbols=model.symbols["variables_labels"],sizes=(3,3),fig_sizes=(10,8),title="Variables") if model_info: from snowdrop.src.misc.text2latex import saveDocument saveDocument(model) if Output: # Save results in excel file fdir = os.path.dirname(fpath) name, ext = os.path.splitext(fpath) if fout is None: fout = os.path.abspath(os.path.join(fdir,'../../../supplements/data/OPT/'+ os.path.basename(name) + '.csv')) with open(fout, 'w') as f: f.writelines(','.join(var_names) + '\n') f.writelines(','.join(str(x) for x in y) +'\n') pTable = PrettyTable(['Var Name','Var Value','Var Name ','Var Value ']) for i in range(n): if i+1 < n: row = [var_names[i], y[i], var_names[1+i], y[1+i]] else: row = [var_names[i], y[i], "", ""] pTable.add_row(row) pTable.float_format = "4.1" print(pTable) if not results.success: cprint(f"\nConstrained solver failed: {results.message}","red") return