Source code for TRXASprefitpack.driver._ampgo

'''
_ampgo:
Adaptive Memory Programing For Global Optimization
Based On Andrea Gavana's Implementation (see: http://infinity77.net/global_optimization/)

For implementation detail see the `Tabu Tunneling Method` section in the below paper.
L. Lasdon et al. Computers & Operations Research 37 (2010) 1500–1509
http://leeds-faculty.colorado.edu/glover/fred%20pubs/416%20-%20AMP%20(TS)%20for%20Constrained%20Global%20Opt%20w%20Lasdon%20et%20al%20.pdf

:copyright: 2021-2022 by pistack (Junho Lee).
'''

from typing import Callable, Optional, Union
import numpy as np
from scipy.optimize import minimize, OptimizeResult

SCIPY_LOCAL_SOLVER = ['Nelder-Mead', 'Powell', 'CG', 'BFGS', 'Newton-CG',
'L-BFGS-B', 'TNC', 'COBYLA', 'SLSQP', 'dogleg', 'trust-ncg']


[docs] def ampgo(fun: Callable, x0: np.ndarray, tot_iter: Optional[int] = 20, max_tunnel: Optional[int] = 5, tol_tunnel: Optional[float] = 1e-5, minimizer_kwargs: Optional[dict] = None, eps1: Optional[float] = 0.02, eps2: Optional[float] = 0.1, n_tabu: Optional[int] = 5, strategy: Optional[str] = 'farthest', seed: Optional[Union[int, np.random.RandomState]] = None, callback: Optional[Callable] = None, disp: Optional[bool] = False) -> OptimizeResult: ''' ampgo: Adaptive Memory Programming for Global Optimization Based on Tabu Tunneling Method Args: fun: Objective function. Objective function should have following form `f(x, *args)` x0: initial guess tot_iter: maximum number of global iteration max_tunnel: maximum number of tunneling phase tol_tunnel: Tolerance to determine whether tunneling phase is successful or not. If :math:`f(x_{best}) \\geq 0` and :math:`f(x_{tunnel}) < (1+{tol})f(x_{best})+{tol}` or :math:`f(x_{best}) < 0` and :math:`f(x_{tunnel}) < (1-{tol})f(x_{best})+{tol}` then such tunneling phase is regarded as successful phase. minimizer_kwargs: Extra keyword arguments to be passed to the local minimizer `scipy.optimize.minimize`. Some important options could be: * method (str): The minimization Method (default: `L-BFGS-B`) * args (tuple): The extra arguments passed to the objective function (`fun`) and its derivatives (`jac`, `hess`) * jac: jacobian of objective function (see scipy.optimize.minimize) * hess: hessian of objective function (see scipy.optimize.minimize) * bounds (Sequence of Tuple): Boundary of variable (see scipy.optimize.minimize) eps1: Constant used to define aspiration value eps2: Perturbation factor used to move away from the latest local minimum n_tabu: size of tabulist strategy ({'farthest', 'oldest'}): The strategy to delete element of tabulist when the size of tabulist exceeds `n_tabu`. * `farthest`: Delete farthest point from the latest local minimum point * `oldest`: Delete oldest point seed: seed used for random perturbation of local minimum. This argument is useful when someone wants to reproduce optimization results. callback: callback function to monitor each global iteration and tunneling phase. function signature should be `callback(x, f_val)` disp: display level, If zero or `None`, then no output is printed on screen. If postive number then status messages are printed. Returns: The optimization results represented as a `scipy.OptimizeResult` object. The important attributes are * `x`: The solution of the optimization * `fun`: values of objective fuction. * `success`: Whether or not the optimizer exited successfuly * `message`: Description of the cause of the termination Note: The implementation of ampgo method is based on L. Lasdon et al. Computers & Operations Research 37 (2010) 1500–1509. and Andrea Gavana's Python Implementation. ''' # initialize nfev = 0 total_tunnel = 0 success_tunnel = 0 f_best = np.inf tabulist = [] tabu_size = 0 x0 = np.atleast_1d(x0) n_param = x0.size ub = np.empty(n_param) lb = np.empty(n_param) monitor = False if isinstance(seed, np.random.RandomState): rng = seed elif isinstance(seed, int): rng = np.random.RandomState(seed) elif seed is None: rng = np.random.RandomState(None) else: raise Exception('Invalid seed for random generator') if callable(callback): monitor = True # Setting local minimizer if minimizer_kwargs is None: method = 'L-BFGS-B' args = () bounds = None jac = None hess = None hessp = None tol = 1e-8 minimizer_kwargs = {} else: method = minimizer_kwargs.pop('method', 'L-BFGS-B') args = minimizer_kwargs.pop('args', ()) bounds = minimizer_kwargs.pop('bounds', None) jac = minimizer_kwargs.pop('jac', None) hess = minimizer_kwargs.pop('hess', None) hessp = minimizer_kwargs.pop('hessp', None) tol = minimizer_kwargs.pop('tol', 1e-8) if method not in SCIPY_LOCAL_SOLVER: raise Exception('Invalid local solver, local solver should be one of' + '[' + ', '.join(SCIPY_LOCAL_SOLVER) + ']') # Setting Arguments For tabu tunneling Function if callable(jac): ttf = wrapper_tunnel(fun, *args) jac_ttf = wrapper_grad_tunnel(fun, jac, *args) elif jac is True: ttf = wrapper_fun_grad_tunnel(fun, *args) jac_ttf = True else: ttf = wrapper_tunnel(fun, *args) jac_ttf = None if bounds is None: for i in range(n_param): ub[i] = np.inf lb[i] = -np.inf else: if len(bounds) != n_param: raise Exception( 'Length of Bounds and the number of paramter should be same') for i in range(n_param): if bounds[i] is None: ub[i] = np.inf lb[i] = -np.inf else: lb[i], ub[i] = bounds[i] if lb[i] is None: lb[i] = -np.inf if ub[i] is None: ub[i] = np.inf # Start main loop for i in range(tot_iter+1): res = minimize(fun, x0, args=args, method=method, jac=jac, hess=hess, hessp=hessp, bounds=bounds, tol=tol, **minimizer_kwargs) f_opt = res['fun'] x0 = res['x'] nfev = nfev + res['nfev'] if monitor: callable(x0, f_opt) if disp: print('='*72) print(f'local minimum is found in global iteration: {i}') if not res['success']: print('Warning: local optimization is failed') if f_opt < f_best: if disp: print( f'local minimum improves solution at global iteration: {i}') print(f'Current: {f_opt} | previous: {f_best}') f_best = f_opt x_best = x0 # If size of tabulist exceeds n_tabu then delete element # Add local minimum solution to the tabulist if tabu_size > n_tabu-1: tabulist = delete_element(x0, tabulist, strategy) tabu_size = n_tabu-1 tabulist.append(x0) tabu_size = tabu_size+1 # Calculates Aspiration aspiration = f_best - eps1*(1+np.abs(f_best)) success = False for j in range(max_tunnel): total_tunnel = total_tunnel+1 if disp: print('='*72) print(f'Tunneling Phase is Started: {i}-{j+1}') # Perturbe local minimum point x0 vaild = False while not vaild: r = rng.uniform(-1, 1, n_param) beta = (eps2*(1+np.linalg.norm(x0)))/np.linalg.norm(r) x_try = x0 + beta*r x_try = np.where(x_try < lb, lb, x_try) x_try = np.where(x_try > ub, ub, x_try) vaild = check_vaild(x_try, tabulist) # start tabu tunneling res = minimize(ttf, x_try, args=(aspiration, tabulist), method=method, jac=jac_ttf, bounds=bounds, tol=tol, **minimizer_kwargs) x0 = res['x'] nfev = nfev + res['nfev'] if jac is True: f_opt, _ = fun(x0, *args) else: f_opt = fun(x0, *args) nfev = nfev+1 if f_best >= 0 and f_opt < (1+tol_tunnel)*f_best+tol_tunnel: success = True success_tunnel = success_tunnel+1 elif f_best < 0 and f_opt < (1-tol_tunnel)*f_best+tol_tunnel: success = True success_tunnel = success_tunnel+1 if monitor: callback(x0, f_opt) if disp and not res['success']: print('Warning: local optimization is failed') if disp and success: print('Tunneling phase is successful') if f_opt < f_best: if disp: print(f'Tunneling phase {i}-{j+1} improves solution') print(f'Current: {f_opt} | previous: {f_best}') f_best = f_opt x_best = x0 # update tabulist if tabu_size > n_tabu-1: delete_element(x0, tabulist, strategy) tabu_size = n_tabu-1 tabulist.append(x0) tabu_size = tabu_size+1 if success: break # Optimization Process is finished result = OptimizeResult() result['fun'] = f_best result['x'] = x_best result['nfev'] = nfev result['success'] = True result['message'] = \ ['Requested Number of global iteration is finished.'] return result
def check_vaild(x_try, tabulist): ''' Check random pertubation of latest local mimum point is vaild. ''' dist = np.sum((tabulist-x_try)**2, axis=1) min_dist = np.min(dist) return min_dist > 1e-16 def delete_element(x_local, tabulist, strategy): ''' Delete element from tabulist ''' if strategy == 'oldest': tabulist.pop(0) else: dist = np.sum((tabulist-x_local)**2, axis=1) idx = np.argmax(dist) tabulist.pop(idx) return tabulist def wrapper_tunnel(fun, *fun_args): ''' wrapper function for tabu tunneling function ''' def tunnel(x0, aspiration, tabulist): numerator = (fun(x0, *fun_args)-aspiration)**2 denumerator = 1 for tabu in tabulist: denumerator = denumerator*np.sum((x0-tabu)**2) return numerator/denumerator return tunnel def wrapper_grad_tunnel(fun, jac, *fun_args): ''' wrapper function for gradient of tabu tunneling function ''' def grad_tunnel(x0, aspiration, tabulist): fval = fun(x0, *fun_args) - aspiration numerator = fval**2 grad_numerator = fval*jac(x0, *fun_args) denominator = 1 grad_denom = np.zeros_like(x0) for tabu in tabulist: diff = tabu-x0 dist = np.sum(diff**2) denominator = denominator*dist grad_denom = grad_denom + diff/dist return 2*(grad_numerator+numerator*grad_denom)/denominator return grad_tunnel def wrapper_fun_grad_tunnel(fun, *fun_args): ''' wrapper function for pair of tabu tunneling function and its gradient ''' def fun_grad_tunnel(x0, aspiration, tabulist): f_val, grad_val = fun(x0, *fun_args) f_val = f_val-aspiration numerator = f_val**2 grad_numerator = f_val*grad_val denominator = 1 grad_denominator = np.zeros_like(x0) for tabu in tabulist: diff = tabu-x0 dist = np.sum(diff**2) denominator = denominator*dist grad_denominator = grad_denominator + \ diff/dist y_ttf = numerator/denominator deriv_y_ttf = 2*(grad_numerator/denominator + y_ttf*grad_denominator) return y_ttf, deriv_y_ttf return fun_grad_tunnel