def metropolis_criterion(self, sol_new: Solution, sol_current: Solution) -> bool: """Apply Metropolis criterion as acceptance decision, return True when sol_new should be accepted.""" if sol_new.is_better(sol_current): return True return np.random.random_sample() <= exp( -abs(sol_new.obj() - sol_current.obj()) / self.temperature)
def update_stats_for_method_pair(self, destroy: Method, repair: Method, sol: Solution, res: Result, obj_old: TObj, t_destroy: float, t_repair: float): """Update statistics, incumbent and check termination condition after having performed a destroy+repair.""" if __debug__ and self.own_settings.mh_checkit: sol.check() ms_destroy = self.method_stats[destroy.name] ms_destroy.applications += 1 ms_destroy.netto_time += t_destroy ms_destroy.brutto_time += t_destroy ms_repair = self.method_stats[repair.name] ms_repair.applications += 1 ms_repair.netto_time += t_repair ms_repair.brutto_time += t_repair obj_new = sol.obj() if sol.is_better_obj(sol.obj(), obj_old): ms_destroy.successes += 1 ms_destroy.obj_gain += obj_new - obj_old ms_repair.successes += 1 ms_repair.obj_gain += obj_new - obj_old self.iteration += 1 new_incumbent = self.update_incumbent(sol, time.process_time() - self.time_start) terminate = self.check_termination() self.log_iteration(destroy.name+'+'+repair.name, obj_old, sol, new_incumbent, terminate, res.log_info) if terminate: self.run_time = time.process_time() - self.time_start res.terminate = True
def copy_from(self, other: 'SubsetVectorSolution'): self.sel = other.sel if self.unselected_elems_in_x(): super().copy_from(other) else: self.x[:self.sel] = other.x[:self.sel] Solution.copy_from(self, other)
def ts(self, sol: Solution): while True: # use of multiple different methods for restricted neighborhood search is possible, # but usually only one is used for m in self.next_method(self.meths_rli, repeat=True): sol_old = sol.copy() def ts_iteration(sol: Solution, _par, result): for ta in self.tabu_list.tabu_list: self.step_logger.info(f'TA: {ta}') m.func(sol, m.par, None, self.tabu_list, self.incumbent) ts_method = Method(m.name, ts_iteration, m.par) t_start = time.process_time() res = self.perform_method(ts_method, sol, delayed_success=True) self.update_tabu_list(sol, sol_old) self.delayed_success_update(m, sol.obj(), t_start, sol_old) for ta in self.tabu_list.tabu_list: self.step_logger.info(f'TA: {ta}') if res.terminate: return
def gvns(self, sol: Solution): """Perform general variable neighborhood search (GVNS) to given solution.""" sol2 = sol.copy() if self.vnd(sol2) or not self.meths_sh: return use_vnd = bool(self.meths_li) while True: for m in self.next_method(self.meths_sh, repeat=True): t_start = time.process_time() res = self.perform_method(m, sol2, delayed_success=use_vnd) terminate = res.terminate if not terminate and use_vnd: terminate = self.vnd(sol2) self.delayed_success_update(m, sol.obj(), t_start, sol2) if sol2.is_better(sol): sol.copy_from(sol2) if terminate or res.terminate: return break else: if terminate or res.terminate: return sol2.copy_from(sol) else: break
def alns(self, sol: Solution): """Perform adaptive large neighborhood search (ALNS) on given solution.""" self.next_segment = self.iteration + self.own_settings.mh_alns_segment_size sol_incumbent = sol.copy() sol_new = sol.copy() while True: destroy, repair = self.select_method_pair() res = self.perform_method_pair(destroy, repair, sol_new) self.update_after_destroy_and_repair_performed( destroy, repair, sol_new, sol_incumbent, sol) if res.terminate: sol.copy_from(sol_incumbent) return self.update_operator_weights()
def delayed_success_update(self, method: Method, obj_old: TObj, t_start: float, sol: Solution): """Update an earlier performed method's success information in method_stats. :param method: earlier performed method :param obj_old: objective value of solution with which to compare to determine success :param t_start: time when the application of method had started :param sol: current solution considered the final result of the method """ t_end = time.process_time() ms = self.method_stats[method.name] ms.brutto_time += t_end - t_start obj_new = sol.obj() if sol.is_better_obj(sol.obj(), obj_old): ms.successes += 1 ms.obj_gain += obj_new - obj_old
def update_tabu_list(self, sol: Solution, sol_old: Solution): ll = self.tabu_list.generate_list_length(self.iteration) # generate list length for current iteration self.step_logger.info(f'LL: {ll}') if self.incumbent_iteration == self.iteration and self.incumbent.is_tabu(self.tabu_list): # a new best solution was found, but it was tabu (aspiration criterion) # get the violated tabu attribute and delete it from the list tabu_violated = sol_old.get_tabu_attribute(self.incumbent) for t in tabu_violated: self.tabu_list.delete_attribute({t}) self.step_logger.info(f'TA_DEL: {tabu_violated}') self.tabu_list.update_list() # updates lifespan of each tabu attribute and deletes expired attributes self.tabu_list.add_attribute(sol.get_tabu_attribute(sol_old), self.tabu_list.current_ll)
def __new__(cls, sol: Solution, meths_ch: List[Method], own_settings: dict = None): """Create population of mh_pop_size solutions using the list of construction heuristics if given. If sol is None or no constructors are given, the population is initialized empty. sol itself is just used as template for obtaining further solutions. """ own_settings = OwnSettings(own_settings) if own_settings else settings size = own_settings.mh_pop_size obj = super(Population, cls).__new__(cls, size, Solution) obj.own_settings = own_settings if sol is not None and meths_ch: # cycle through construction heuristics to generate population # perform all construction heuristics, take best solution meths_cycle = cycle(meths_ch) idx = 0 while idx < size: m = next(meths_cycle) sol = sol.copy() res = Result() m.func(sol, m.par, res) if own_settings.mh_pop_dupelim and obj.duplicates_of( sol) != []: continue # do not add this duplicate obj[idx] = sol if res.terminate: break idx += 1 return obj
def perform_methods(self, methods: List[Method], sol: Solution) -> Result: """Performs all methods on given solution and returns Results object. Also updates incumbent, iteration and the method's statistics in method_stats. Furthermore checks the termination condition and eventually sets terminate in the returned Results object. :param methods: list of methods to perform :param sol: solution to which the method is applied :returns: Results object """ res = Result() obj_old = sol.obj() method_name = "" for method in methods: if method_name != "": method_name += "+" method_name += method.name method.func(sol, method.par, res) if res.terminate: break t_end = time.process_time() self.iteration += 1 new_incumbent = self.update_incumbent(sol, t_end - self.time_start) terminate = self.check_termination() self.log_iteration(method_name, obj_old, sol, new_incumbent, terminate, res.log_info) if terminate: self.run_time = time.process_time() - self.time_start res.terminate = True return res
def __init__(self, sol: Solution, meths_ch: List[Method], meths_destroy: List[Method], meths_repair: List[Method], own_settings: dict = None, consider_initial_sol=False): """Initialization. :param sol: solution to be improved :param meths_ch: list of construction heuristic methods :param meths_destroy: list of destroy methods :param meths_repair: list of repair methods :param own_settings: optional dictionary with specific settings :param consider_initial_sol: if true consider sol as valid solution that should be improved upon; otherwise sol is considered just a possibly uninitialized of invalid solution template """ super().__init__(sol, meths_ch + meths_destroy + meths_repair, own_settings, consider_initial_sol) self.meths_ch = meths_ch assert meths_destroy and meths_repair self.meths_destroy = meths_destroy self.meths_repair = meths_repair self.score_data = { m.name: ScoreData() for m in chain(self.meths_destroy, self.meths_repair) } self.temperature = sol.obj( ) * self.own_settings.mh_alns_init_temp_factor + 0.000000001 self.next_segment = 0
def sa_iteration(sol: Solution, _par, result): neighborhood_move, delta_obj = self.random_move_delta_eval(sol) acceptance = self.metropolis_criterion(sol, delta_obj) if acceptance: self.apply_neighborhood_move(sol, neighborhood_move) sol.obj_val = sol.obj_val + delta_obj result.changed = True if self.iter_cb is not None: self.iter_cb(self.iteration, sol, self.temperature, acceptance)
def vnd(self, sol: Solution) -> bool: """Perform variable neighborhood descent (VND) on given solution. :returns: true if a global termination condition is fulfilled, else False. """ sol2 = sol.copy() while True: for m in self.next_method(self.meths_li): res = self.perform_method(m, sol2) if sol2.is_better(sol): sol.copy_from(sol2) if res.terminate: return True break if res.terminate: return True if res.changed: sol2.copy_from(sol) else: # local optimum reached return False
def alns(self, sol: Solution): """Perform adaptive large neighborhood search (ALNS) on given solution.""" self.next_segment = self.iteration + self.own_settings.mh_alns_segment_size sol_incumbent = sol.copy() sol_new = sol.copy() operators = self.operators_generator(sol_new) worker_seed = 0 if settings.mh_workers > 1 else settings.seed with mp.Pool(processes=settings.mh_workers, initializer=self.process_init, initargs=(settings, worker_seed)) as worker_pool: result_iter = worker_pool.imap_unordered( self.perform_method_pair_in_worker, operators) for result in result_iter: # print("Result:", result) destroy, repair, sol_result, res, obj_old, t_destroy, t_repair = result sol_new.copy_from(sol_result) self.update_stats_for_method_pair(destroy, repair, sol, res, obj_old, t_destroy, t_repair) self.update_after_destroy_and_repair_performed( destroy, repair, sol_new, sol_incumbent, sol) if res.terminate: sol.copy_from(sol_incumbent) return self.update_operator_weights()
def perform_method(self, method: Method, sol: Solution, delayed_success=False) -> Result: """Perform method on given solution and returns Results object. Also updates incumbent, iteration and the method's statistics in method_stats. Furthermore checks the termination condition and eventually sets terminate in the returned Results object. :param method: method to be performed :param sol: solution to which the method is applied :param delayed_success: if set the success is not immediately determined and updated but at some later call of delayed_success_update() :returns: Results object """ res = Result() obj_old = sol.obj() ##### logging for visualisation if self.step_logger.hasHandlers(): sol_str, inc_str = f'{sol}'.replace( '\n', ' '), f'{self.incumbent}'.replace('\n', ' ') step_info = f'START\nSOL: {sol_str}\nOBJ: {obj_old}\nM: {method.name}\n' + \ f'PAR: {method.par}\nINC: {inc_str}\nBEST: {self.incumbent.obj()}' self.step_logger.info(step_info) ################# t_start = time.process_time() method.func(sol, method.par, res) t_end = time.process_time() if __debug__ and self.own_settings.mh_checkit: sol.check() ms = self.method_stats[method.name] ms.applications += 1 ms.netto_time += t_end - t_start obj_new = sol.obj() if not delayed_success: ms.brutto_time += t_end - t_start if sol.is_better_obj(sol.obj(), obj_old): ms.successes += 1 ms.obj_gain += obj_new - obj_old self.iteration += 1 new_incumbent = self.update_incumbent(sol, t_end - self.time_start) ##### logging for visualisation if self.step_logger.hasHandlers(): sol_str, inc_str = f'{sol}'.replace( '\n', ' '), f'{self.incumbent}'.replace('\n', ' ') step_info = f'END\nSOL: {sol_str}\nOBJ: {sol.obj()}\nM: {method.name}\n' + \ f'PAR: {method.par}\nINC: {inc_str}\nBEST: {self.incumbent.obj()}\nBETTER: {new_incumbent}' self.step_logger.info(step_info) ################# terminate = self.check_termination() self.log_iteration(method.name, obj_old, sol, new_incumbent, terminate, res.log_info) if terminate: self.run_time = time.process_time() - self.time_start res.terminate = True return res
def perform_method_pair(self, destroy: Method, repair: Method, sol: Solution) -> Result: """Performs a destroy/repair method pair on given solution and returns Results object. Also updates incumbent, iteration and the method's statistics in method_stats. Furthermore checks the termination condition and eventually sets terminate in the returned Results object. :param destroy: destroy destroy method to be performed :param repair: repair destroy method to be performed :param sol: solution to which the method is applied :returns: Results object """ res = Result() obj_old = sol.obj() t_start = time.process_time() destroy.func(sol, destroy.par, res) t_destroyed = time.process_time() repair.func(sol, repair.par, res) t_end = time.process_time() self.update_stats_for_method_pair(destroy, repair, sol, res, obj_old, t_destroyed - t_start, t_end - t_destroyed) return res
def perform_method(self, method: Method, sol: Solution, delayed_success=False) -> Result: """Perform method on given solution and returns Results object. Also updates incumbent, iteration and the method's statistics in method_stats. Furthermore checks the termination condition and eventually sets terminate in the returned Results object. :param method: method to be performed :param sol: solution to which the method is applied :param delayed_success: if set the success is not immediately determined and updated but at some later call of delayed_success_update() :returns: Results object """ res = Result() obj_old = sol.obj() t_start = time.process_time() method.func(sol, method.par, res) t_end = time.process_time() if __debug__ and self.own_settings.mh_checkit: sol.check() ms = self.method_stats[method.name] ms.applications += 1 ms.netto_time += t_end - t_start obj_new = sol.obj() if not delayed_success: ms.brutto_time += t_end - t_start if sol.is_better_obj(sol.obj(), obj_old): ms.successes += 1 ms.obj_gain += obj_new - obj_old self.iteration += 1 new_incumbent = self.update_incumbent(sol, t_end - self.time_start) terminate = self.check_termination() self.log_iteration(method.name, obj_old, sol, new_incumbent, terminate, res.log_info) if terminate: self.run_time = time.process_time() - self.time_start res.terminate = True return res
def update_after_destroy_and_repair_performed(self, destroy: Method, repair: Method, sol_new: Solution, sol_incumbent: Solution, sol: Solution): """Update current solution, incumbent, and all operator score data according to performed destroy+repair. :param destroy: applied destroy method :param repair: applied repair method :param sol_new: obtained new solution :param sol_incumbent: current incumbent solution :param sol: current (last accepted) solution """ destroy_data = self.score_data[destroy.name] repair_data = self.score_data[repair.name] destroy_data.applied += 1 repair_data.applied += 1 score = 0 if sol_new.is_better(sol_incumbent): score = self.own_settings.mh_alns_sigma1 # print('better than incumbent') sol_incumbent.copy_from(sol_new) sol.copy_from(sol_new) elif sol_new.is_better(sol): score = self.own_settings.mh_alns_sigma2 # print('better than current') sol.copy_from(sol_new) elif sol.is_better(sol_new) and self.metropolis_criterion( sol_new, sol): score = self.own_settings.mh_alns_sigma3 # print('accepted although worse') sol.copy_from(sol_new) elif sol_new != sol: sol_new.copy_from(sol) destroy_data.score += score repair_data.score += score