├── main.py ├── utils.py ├── dimacs_parser.py ├── .gitignore ├── gsat.py ├── walksat.py ├── gsat_tabu.py ├── base_solver.py ├── novelty.py ├── walksat_tabu.py ├── robust_tabu_search.py ├── full_basic_walksat_solver.py ├── hamming_reactive_tabu_search.py ├── r_novelty.py ├── adaptive_novelty.py ├── iterated_robust_tabu_search.py ├── README.md └── adaptive_memory_LS.py /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np 4 | from utils import get_args 5 | from full_basic_walksat_solver import WalkSAT_Solver 6 | 7 | def main(): 8 | try: 9 | args = get_args() 10 | input_cnf_file = args.input 11 | verbose = args.verbose 12 | except: 13 | print("missing or invalid arguments") 14 | exit(0) 15 | 16 | solver = WalkSAT_Solver(input_cnf_file, verbose) 17 | solver.solve() 18 | 19 | if __name__ == '__main__': 20 | main() -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | def get_args(): 4 | argparser = argparse.ArgumentParser(description=__doc__) 5 | argparser.add_argument( 6 | '-i', '--input', 7 | metavar='I', 8 | # default='cnf_instances/Steiner-9-5-bce.cnf', 9 | # default='cnf_instances/Steiner-15-7-bce.cnf', 10 | # default='cnf_instances/Steiner-27-10-bce.cnf', 11 | # default='cnf_instances/test.cnf', 12 | default='cnf_instances/uf20-01.cnf', 13 | # default='cnf_instances/uf50-01.cnf', 14 | # default='cnf_instances/uuf50-01.cnf', 15 | # default='cnf_instances/uf50-06.cnf', 16 | # default='cnf_instances/uuf100-UNSAT.cnf', 17 | help='The DIMACS file') 18 | argparser.add_argument( 19 | '-v', '--verbose', 20 | default=1, 21 | help='Verbose option') 22 | args = argparser.parse_args() 23 | return args -------------------------------------------------------------------------------- /dimacs_parser.py: -------------------------------------------------------------------------------- 1 | 2 | ''' 3 | DIMAC parser: read CNF file and save in a list 4 | Reference: https://github.com/marcmelis/dpll-sat/blob/master/solvers/original_dpll.py 5 | ''' 6 | import time 7 | 8 | def parse(filename, verbose): 9 | initial_time = time.time() 10 | clauses = [] 11 | if verbose: 12 | print('=====================[ Problem Statistics ]=====================') 13 | print('| |') 14 | for line in open(filename): 15 | if line.startswith('c'): continue 16 | if line.startswith('p'): 17 | nvars, nclauses = line.split()[2:4] 18 | if verbose: 19 | print('| Nb of variables: {0:10s} |'.format(nvars)) 20 | print('| Nb of clauses: {0:10s} |'.format(nclauses)) 21 | continue 22 | clause = [int(x) for x in line[:-2].split()] 23 | if len(clause) > 0: 24 | clauses.append(clause) 25 | 26 | end_time = time.time() 27 | if verbose: 28 | print('| Parse time: {0:10.4f}s |'.format(end_time - initial_time)) 29 | print('| |') 30 | 31 | return clauses, int(nvars) 32 | 33 | # # Unit test 34 | # cnf, maxvar = parse("cnf_instances/test.cnf") 35 | # print(cnf, maxvar) 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Local folders 132 | cnf_instances/* 133 | .vscode/settings.json 134 | unit_tests/* 135 | -------------------------------------------------------------------------------- /gsat.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] B. Selman, H. Levesque, and D. Mitchell, “A New Method for Solving Hard Satisfiability Problems,” Proc. Tenth Natl. Conf. Artif. Intell., no. July, pp. 440–446, 1992. 4 | ''' 5 | 6 | from base_solver import Base_Solver 7 | import numpy as np 8 | import random 9 | import time 10 | from itertools import chain 11 | 12 | class GSAT(Base_Solver): 13 | 14 | def __init__(self, input_cnf_file, verbose, random_walk = False, noise_parameter = 0.2): 15 | super(GSAT, self).__init__(input_cnf_file, verbose) 16 | self.random_walk = random_walk 17 | self.noise_parameter = noise_parameter 18 | 19 | def solve(self): 20 | initial = time.time() 21 | self.initialize_pool() 22 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 23 | self.generate() 24 | self.initialize_cost() 25 | while self.nb_flips < self.MAX_FLIPS and not self.is_sat: 26 | if self.check() == 1: # if no unsat clause => finish 27 | self.is_sat = True 28 | else: 29 | assert len(self.id_unsat_clauses) > 0 30 | ''' 31 | - GSAT idea (Intensification => focus on best var) 32 | - Among all variables that occur in unsat clauses 33 | - Choose a variable x which minimizes cost to flip 34 | ''' 35 | all_unsat_lits = [] 36 | for ind in self.id_unsat_clauses: 37 | all_unsat_lits += self.list_clauses[ind] 38 | all_unsat_lits = list(set(all_unsat_lits)) # flatten & remove redundants 39 | ''' 40 | Compute cost when flipping each literal 41 | Cost = break - make 42 | ''' 43 | break_count = [] 44 | for literal in all_unsat_lits: 45 | break_count.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 46 | ''' 47 | Random walk 48 | ''' 49 | if self.random_walk: 50 | p = random.random() 51 | if p < self.noise_parameter: # pick x randomly from literals in all unsat clause 52 | x = random.choice(all_unsat_lits) 53 | else: 54 | x = all_unsat_lits[np.argmin(break_count)] 55 | else: 56 | x = all_unsat_lits[np.argmin(break_count)] 57 | self.flip(x) 58 | 59 | end = time.time() 60 | print('Nb flips: {0} '.format(self.nb_flips)) 61 | print('Nb tries: {0} '.format(self.nb_tries)) 62 | print('CPU time: {0:10.4f} s '.format(end-initial)) 63 | if self.is_sat: 64 | print('SAT') 65 | return self.assignment 66 | else: 67 | print('UNKNOWN') 68 | return None 69 | 70 | 71 | -------------------------------------------------------------------------------- /walksat.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] B. Selman, H. Kautz, and B. Cohen, “Noise strategies for local search,” AAAI/IAAI Proc., no. 1990, pp. 337–343, 1994. 4 | ''' 5 | 6 | from base_solver import Base_Solver 7 | import numpy as np 8 | import random 9 | import time 10 | from itertools import chain 11 | 12 | class WalkSAT(Base_Solver): 13 | 14 | def __init__(self, input_cnf_file, verbose, SKC = True, random_walk = False, noise_parameter = 0.2): 15 | super(WalkSAT, self).__init__(input_cnf_file, verbose) 16 | self.SKC = SKC 17 | self.random_walk = random_walk 18 | self.noise_parameter = noise_parameter 19 | 20 | def pick_unsat_clause(self): 21 | assert len(self.id_unsat_clauses) > 0 22 | random_index = random.choice(self.id_unsat_clauses) 23 | return self.list_clauses[random_index] 24 | 25 | def solve(self): 26 | initial = time.time() 27 | self.initialize_pool() 28 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 29 | self.generate() 30 | self.initialize_cost() 31 | while self.nb_flips < self.MAX_FLIPS and not self.is_sat: 32 | if self.check() == 1: # if no unsat clause => finish 33 | self.is_sat = True 34 | else: 35 | assert len(self.id_unsat_clauses) > 0 36 | ''' 37 | - WalkSAT idea 38 | - Among all variables that occur in unsat clauses, pick one randomly ! (=> Diverisification) 39 | - Choose a variable x which minimizes "break count" in this unsat clause to flip 40 | ''' 41 | unsat_clause = self.pick_unsat_clause() 42 | break_count = [] 43 | for literal in unsat_clause: 44 | break_count.append(self.evaluate_breakcount(literal, bs=1, ms=0)) 45 | ''' 46 | Original WalkSAT proposed by Selman, Kautz, and Cohen (1994). 47 | "never make a random move if there exists one literal with zero break-count" 48 | ''' 49 | if self.SKC and (0 in break_count): 50 | x = unsat_clause[break_count.index(0)] 51 | else: 52 | ''' 53 | Random walk 54 | ''' 55 | if self.random_walk: 56 | p = random.random() 57 | if p < self.noise_parameter: # pick x randomly from literals in all unsat clause 58 | x = random.choice(unsat_clause) 59 | else: 60 | x = unsat_clause[np.argmin(break_count)] 61 | else: 62 | x = unsat_clause[np.argmin(break_count)] 63 | self.flip(x) 64 | 65 | end = time.time() 66 | print('Nb flips: {0} '.format(self.nb_flips)) 67 | print('Nb tries: {0} '.format(self.nb_tries)) 68 | print('CPU time: {0:10.4f} s '.format(end-initial)) 69 | if self.is_sat: 70 | print('SAT') 71 | return self.assignment 72 | else: 73 | print('UNKNOWN') 74 | return None 75 | 76 | 77 | -------------------------------------------------------------------------------- /gsat_tabu.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] D. McAllester, B. Selman, and H. Kautz, “Evidence for invariants in local search,” Proc. Natl. Conf. Artif. Intell., pp. 321–326, 1997. 4 | ''' 5 | 6 | from base_solver import Base_Solver 7 | import numpy as np 8 | import random 9 | import time 10 | from itertools import chain 11 | 12 | class GSAT_Tabu(Base_Solver): 13 | 14 | def __init__(self, input_cnf_file, verbose, random_walk = False, noise_parameter = 0.2, tabu_length=None): 15 | super(GSAT_Tabu, self).__init__(input_cnf_file, verbose) 16 | self.random_walk = random_walk 17 | self.noise_parameter = noise_parameter 18 | ''' 19 | Initialize tabu list and its length 20 | Note that tabu list is a circular list 21 | ''' 22 | if tabu_length is None: 23 | self.tabu_length = int(0.01875*self.nvars + 2.8125) 24 | else: 25 | self.tabu_length = tabu_length 26 | self.tabu_list = [] 27 | 28 | def add_tabu(self, literal): 29 | ''' 30 | Add a move to tabu list 31 | ''' 32 | if len(self.tabu_list) < self.tabu_length: 33 | self.tabu_list.append(abs(literal)) 34 | else: # tabu list is full 35 | self.tabu_list.pop(0) 36 | self.tabu_list.append(literal) 37 | 38 | def pick_all_lits(self,id_unsat_clauses, tabu_list=None): 39 | all_allowed_lits = [] 40 | for ind in id_unsat_clauses: 41 | all_allowed_lits += self.list_clauses[ind] 42 | if tabu_list is not None: 43 | all_allowed_lits = list(set(all_allowed_lits)^set(tabu_list)) 44 | else: 45 | all_allowed_lits = list(set(all_allowed_lits)) 46 | return all_allowed_lits 47 | 48 | def solve(self): 49 | initial = time.time() 50 | self.initialize_pool() 51 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 52 | self.generate() 53 | self.initialize_cost() 54 | while self.nb_flips < self.MAX_FLIPS and not self.is_sat: 55 | if self.check() == 1: # if no unsat clause => finish 56 | self.is_sat = True 57 | else: 58 | assert len(self.id_unsat_clauses) > 0 59 | ''' 60 | - GSAT idea (Intensification => focus on best var) 61 | - Among all variables that occur in unsat clauses 62 | - Choose a variable x which minimizes cost to flip 63 | ''' 64 | # compute allowed literals wrt tabu list 65 | all_allowed_lits = self.pick_all_lits(self.id_unsat_clauses, self.tabu_list) 66 | if len(all_allowed_lits) == 0: # else take all_allowed_lits and ignore tabu 67 | all_allowed_lits = self.pick_all_lits(self.id_unsat_clauses) 68 | ''' 69 | Compute cost when flipping each literal 70 | Cost = break - make 71 | ''' 72 | break_count = [] 73 | for literal in all_allowed_lits: 74 | break_count.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 75 | ''' 76 | Random walk 77 | ''' 78 | if self.random_walk: 79 | p = random.random() 80 | if p < self.noise_parameter: # pick x randomly from literals in all unsat clause 81 | x = random.choice(all_allowed_lits) 82 | else: 83 | x = all_allowed_lits[np.argmin(break_count)] 84 | else: 85 | x = all_allowed_lits[np.argmin(break_count)] 86 | 87 | self.flip(x) 88 | self.add_tabu(x) 89 | 90 | end = time.time() 91 | print('Nb flips: {0} '.format(self.nb_flips)) 92 | print('Nb tries: {0} '.format(self.nb_tries)) 93 | print('CPU time: {0:10.4f} s '.format(end-initial)) 94 | if self.is_sat: 95 | print('SAT') 96 | return self.assignment 97 | else: 98 | print('UNKNOWN') 99 | return None 100 | 101 | 102 | -------------------------------------------------------------------------------- /base_solver.py: -------------------------------------------------------------------------------- 1 | from dimacs_parser import parse 2 | import numpy as np 3 | import random 4 | import time 5 | 6 | class Base_Solver: 7 | 8 | def __init__(self, input_cnf_file, verbose): 9 | self.list_clauses, self.nvars = parse(input_cnf_file, verbose) 10 | self.verbose = verbose 11 | self.assignment = [] 12 | self.pool = dict() #key: literal -> element: index of clause which contains literal 13 | self.id_unsat_clauses = [] # save id of unsat clause 14 | self.costs = np.zeros(len(self.list_clauses)) #compute nb of literals make clause true (i.e. for clause Ci, if fi>0 => T, fi==0 => F) 15 | self.MAX_TRIES = 50 16 | self.MAX_FLIPS = 100*self.nvars 17 | self.nb_tries = 0 18 | self.nb_flips = 0 19 | self.is_sat = False 20 | 21 | for clause in self.list_clauses: 22 | assert len(clause) > 0 23 | 24 | def generate(self): 25 | self.assignment = [] 26 | self.nb_tries += 1 27 | self.nb_flips = 0 28 | for x in range(1, self.nvars+1): 29 | choice = [-1,1] 30 | self.assignment.append(x * random.choice(choice)) 31 | 32 | def initialize_pool(self): 33 | for i, clause in enumerate(self.list_clauses): 34 | for literal in clause: 35 | if literal in self.pool.keys(): 36 | self.pool[literal].append(i) 37 | else: 38 | self.pool[literal] = [i] 39 | 40 | def initialize_cost(self): 41 | # Compute nb of literals make clause true (i.e. for clause Ci, if fi>0 => T, fi==0 => F) 42 | # Let's call it cost ! 43 | assert len(self.assignment) > 0 44 | self.id_unsat_clauses = [] 45 | for i, clause in enumerate(self.list_clauses): 46 | self.costs[i] = 0 47 | for literal in clause: 48 | if literal in self.assignment: 49 | self.costs[i] += 1 50 | if self.costs[i] == 0: #Clause[i] is currently UNSAT 51 | self.id_unsat_clauses.append(i) 52 | 53 | def check(self): 54 | # check if all is SAT 55 | return len(self.id_unsat_clauses) == 0 56 | 57 | def evaluate_breakcount(self, literal, bs=1, ms=1): 58 | # Compute the breakcount score: #clause which turn SAT -> UNSAT 59 | ind = 0 60 | if literal in self.assignment: 61 | ind = self.assignment.index(literal) 62 | elif -literal in self.assignment: 63 | ind = self.assignment.index(-literal) 64 | original_literal = self.assignment[ind] 65 | # when flipping literal -> -literal 66 | # For every clause which contains literal => cost-- 67 | breakcount = 0 68 | if original_literal in self.pool.keys(): 69 | for i in self.pool[original_literal]: 70 | if self.costs[i] == 1: 71 | breakcount += 1 72 | # For every clause which contains -literal => cost ++ 73 | makecount = 0 74 | if -original_literal in self.pool.keys(): 75 | for j in self.pool[-original_literal]: 76 | if self.costs[j] == 0: 77 | makecount += 1 78 | # Score = break - make 79 | score = bs*breakcount - ms*makecount 80 | return score 81 | 82 | def flip(self, literal): 83 | self.nb_flips += 1 84 | # Flip variable in assignment 85 | ind = 0 86 | if literal in self.assignment: 87 | ind = self.assignment.index(literal) 88 | elif -literal in self.assignment: 89 | ind = self.assignment.index(-literal) 90 | old_literal = self.assignment[ind] 91 | self.assignment[ind] *= -1 92 | # Update cost 93 | # Clause contains literal => cost -- 94 | if old_literal in self.pool.keys(): 95 | for i in self.pool[old_literal]: 96 | self.costs[i] -= 1 97 | if self.costs[i] == 0: # if SAT -> UNSAT: add to list of unsat clauses 98 | self.id_unsat_clauses.append(i) 99 | # Clause contains -literal => cost ++ 100 | if -old_literal in self.pool.keys(): 101 | for j in self.pool[-old_literal]: 102 | if self.costs[j] == 0: # if UNSAT -> SAT: remove from list of unsat clauses 103 | self.id_unsat_clauses.remove(j) 104 | self.costs[j] += 1 105 | 106 | def solve(self): 107 | raise NotImplementedError -------------------------------------------------------------------------------- /novelty.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] D. McAllester, B. Selman, and H. Kautz, “Evidence for invariants in local search,” Proc. Natl. Conf. Artif. Intell., pp. 321–326, 1997. 4 | [2] H. H. Hoos and T. Stützle, “Towards a characterization of the behaviour of stochastic local search algorithms for SAT,” Artif. Intell., vol. 112, no. 1, pp. 213–232, 1999, doi: 10.1016/S0004-3702(99)00048-X. 5 | ''' 6 | 7 | from base_solver import Base_Solver 8 | import numpy as np 9 | import random 10 | import time 11 | from itertools import chain 12 | 13 | class Novelty(Base_Solver): 14 | 15 | def __init__(self, input_cnf_file, verbose, noise_parameter = 0.2, random_walk_noise = None): 16 | super(Novelty, self).__init__(input_cnf_file, verbose) 17 | self.noise_parameter = noise_parameter 18 | self.most_recent = None 19 | # Introduce random walk noise parameter => Novelty+ 20 | self.random_walk_noise = random_walk_noise 21 | 22 | def solve(self): 23 | initial = time.time() 24 | self.initialize_pool() 25 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 26 | self.generate() 27 | self.initialize_cost() 28 | self.most_recent = None 29 | while self.nb_flips < self.MAX_FLIPS and not self.is_sat: 30 | if self.check() == 1: # if no unsat clause => finish 31 | self.is_sat = True 32 | else: 33 | assert len(self.id_unsat_clauses) > 0 34 | ''' 35 | - GSAT idea (Intensification => focus on best var) 36 | - Among all variables that occur in unsat clauses 37 | - Choose a variable x which minimizes cost to flip 38 | ''' 39 | all_unsat_lits = [] 40 | for ind in self.id_unsat_clauses: 41 | all_unsat_lits += self.list_clauses[ind] 42 | all_unsat_lits = list(set(all_unsat_lits)) # flatten & remove redundants 43 | ''' 44 | Compute cost when flipping each literal 45 | cost = break - make 46 | Best var with min cost 47 | ''' 48 | cost = [] 49 | for literal in all_unsat_lits: 50 | cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 51 | ''' 52 | Random walk 53 | ''' 54 | apply_novelty = True 55 | if self.random_walk_noise is not None: 56 | wp = random.random() 57 | if wp < self.random_walk_noise: 58 | x = random.choice(all_unsat_lits) 59 | apply_novelty = False 60 | ''' 61 | [Novelty strategy] 62 | Arrange variables according to each cost 63 | (1) If the best one *x1* is NOT the most recently flipped variable => select *x1*. 64 | Otherwise, 65 | (2a) select *x2* with probability p, 66 | (2b) select *x1* with probability 1-p. 67 | ''' 68 | if apply_novelty: 69 | if len(all_unsat_lits) == 1: 70 | x = all_unsat_lits[0] 71 | else: 72 | best_id = np.argmin(cost) 73 | best_var = all_unsat_lits[best_id] 74 | cost.pop(best_id) 75 | all_unsat_lits.remove(best_var) 76 | second_best_id = np.argmin(cost) 77 | second_best_var = all_unsat_lits[second_best_id] 78 | # best_var, second_best_var = all_unsat_lits[0], all_unsat_lits[1] 79 | if abs(best_var) != self.most_recent: #(1) 80 | x = best_var 81 | else: 82 | p = random.random() 83 | if p < self.noise_parameter: #(2a) 84 | x = second_best_var 85 | else: #(2b) 86 | x = best_var 87 | ''' 88 | After choosing x, flip it and save this last recent move 89 | ''' 90 | self.flip(x) 91 | self.most_recent = abs(x) 92 | 93 | end = time.time() 94 | print('Nb flips: {0} '.format(self.nb_flips)) 95 | print('Nb tries: {0} '.format(self.nb_tries)) 96 | print('CPU time: {0:10.4f} s '.format(end-initial)) 97 | if self.is_sat: 98 | print('SAT') 99 | return self.assignment 100 | else: 101 | print('UNKNOWN') 102 | return None 103 | 104 | 105 | -------------------------------------------------------------------------------- /walksat_tabu.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] D. McAllester, B. Selman, and H. Kautz, “Evidence for invariants in local search,” Proc. Natl. Conf. Artif. Intell., pp. 321–326, 1997. 4 | ''' 5 | 6 | from base_solver import Base_Solver 7 | import numpy as np 8 | import random 9 | import time 10 | from itertools import chain 11 | 12 | class WalkSAT_Tabu(Base_Solver): 13 | 14 | def __init__(self, input_cnf_file, verbose, SKC = True, random_walk = False, noise_parameter = 0.2, tabu_length=None): 15 | super(WalkSAT_Tabu, self).__init__(input_cnf_file, verbose) 16 | self.SKC = SKC 17 | self.random_walk = random_walk 18 | self.noise_parameter = noise_parameter 19 | ''' 20 | Initialize tabu list and its length 21 | Note that tabu list is a circular list 22 | ''' 23 | if tabu_length is None: 24 | self.tabu_length = int(0.01875*self.nvars + 2.8125) 25 | else: 26 | self.tabu_length = tabu_length 27 | self.tabu_list = [] 28 | 29 | def add_tabu(self, literal): 30 | ''' 31 | Add a move to tabu list 32 | ''' 33 | if len(self.tabu_list) < self.tabu_length: 34 | self.tabu_list.append(abs(literal)) 35 | else: # tabu list is full 36 | self.tabu_list.pop(0) 37 | self.tabu_list.append(literal) 38 | 39 | def solve(self): 40 | initial = time.time() 41 | self.initialize_pool() 42 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 43 | self.generate() 44 | self.initialize_cost() 45 | while self.nb_flips < self.MAX_FLIPS and not self.is_sat: 46 | if self.check() == 1: # if no unsat clause => finish 47 | self.is_sat = True 48 | else: 49 | assert len(self.id_unsat_clauses) > 0 50 | ''' 51 | - WalkSAT idea 52 | - Among all variables that occur in unsat clauses, pick one randomly ! (=> Diverisification) 53 | - Choose a variable x which minimizes break count in this unsat clause to flip 54 | - When integrating a tabu list, first find an unsat clause that has at least 1 non-tabu variable 55 | - Otherwise pick next clause 56 | - When all candidates are tabus => ignore tabu list 57 | ''' 58 | list_id_unsat_clauses = self.id_unsat_clauses.copy() ## Attention: Copy a list => avoid destroying original list 59 | unsat_clause = [] 60 | while len(unsat_clause) == 0 and len(list_id_unsat_clauses) > 0: 61 | random_id = random.choice(list_id_unsat_clauses) 62 | list_id_unsat_clauses.remove(random_id) 63 | unsat_clause = self.list_clauses[random_id] 64 | unsat_clause = list(set(unsat_clause)^set(self.tabu_list)) 65 | if len(unsat_clause) == 0: #ignore 66 | random_id = random.choice(self.id_unsat_clauses) 67 | unsat_clause = self.list_clauses[random_id] 68 | assert len(unsat_clause) > 0 69 | ''' 70 | Compute "break-count" 71 | ''' 72 | break_count = [] 73 | for literal in unsat_clause: 74 | break_count.append(self.evaluate_breakcount(literal, bs=1, ms=0)) 75 | ''' 76 | Original WalkSAT proposed by Selman, Kautz, and Cohen (1994). 77 | "never make a random move if there exists one literal with zero break-count" 78 | ''' 79 | if self.SKC and (0 in break_count): 80 | x = unsat_clause[break_count.index(0)] 81 | else: 82 | ''' 83 | Random walk 84 | ''' 85 | if self.random_walk: 86 | p = random.random() 87 | if p < self.noise_parameter: # pick x randomly from literals in all unsat clause 88 | x = random.choice(unsat_clause) 89 | else: 90 | x = unsat_clause[np.argmin(break_count)] 91 | else: 92 | x = unsat_clause[np.argmin(break_count)] 93 | ''' 94 | Flip chosen variable 95 | Add this variable to taby list 96 | ''' 97 | self.flip(x) 98 | self.add_tabu(x) 99 | 100 | end = time.time() 101 | print('Nb flips: {0} '.format(self.nb_flips)) 102 | print('Nb tries: {0} '.format(self.nb_tries)) 103 | print('CPU time: {0:10.4f} s '.format(end-initial)) 104 | if self.is_sat: 105 | print('SAT') 106 | return self.assignment 107 | else: 108 | print('UNKNOWN') 109 | return None 110 | 111 | 112 | -------------------------------------------------------------------------------- /robust_tabu_search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] K. Smyth, H. H. Hoos, and T. Stützle, “Iterated robust tabu search for MAX-SAT,” in Lecture Notes in Computer Science (including subseries Lecture Notes in Artificial Intelligence and Lecture Notes in Bioinformatics), 2003, vol. 2671, pp. 129–144, doi: 10.1007/3-540-44886-1_12. 4 | [2] E. Taillard, “Robust taboo search for the quadratic assignment problem,” Parallel Comput., vol. 17, no. 4–5, pp. 443–455, 1991, doi: 10.1016/S0167-8191(05)80147-4. 5 | ''' 6 | 7 | from base_solver import Base_Solver 8 | import numpy as np 9 | import random 10 | import time 11 | from itertools import chain 12 | 13 | class RoTS(Base_Solver): 14 | 15 | def __init__(self, input_cnf_file, verbose): 16 | super(RoTS, self).__init__(input_cnf_file, verbose) 17 | ''' 18 | Instead of using a circular list, use a list for tracking last move of each variable 19 | If current_time - last_move < tabu_tenure => a tabu move ! 20 | Else => non-tabu moves 21 | Initialize all by -1 22 | ''' 23 | self.tabu_tenure = int(self.nvars/10 + 4) 24 | self.tabu_tenure_MIN = int(self.nvars/10) 25 | self.tabu_tenure_MAX = int(self.nvars/10) * 3 26 | self.last_move = [-1 for _ in self.assignment] 27 | self.best_cost = len(self.list_clauses) 28 | self.CHECK_FREQ = self.nvars * 10 29 | 30 | def pick_allowed_lits(self,id_unsat_clauses, tabu=True): 31 | all_allowed_lits = [] 32 | non_allowed_lits = [] 33 | for ind in id_unsat_clauses: 34 | all_allowed_lits += self.list_clauses[ind] 35 | all_allowed_lits = list(set(all_allowed_lits)) 36 | if tabu: 37 | for lit in all_allowed_lits: 38 | if self.nb_flips - self.last_move[abs(lit)-1] < self.tabu_tenure: #tabu move 39 | all_allowed_lits.remove(lit) 40 | non_allowed_lits.append(lit) 41 | return all_allowed_lits, non_allowed_lits 42 | 43 | def pick_necessary_flip(self): 44 | oldest_move = min(self.last_move) 45 | if self.nb_flips - oldest_move > self.CHECK_FREQ: 46 | return self.assignment[self.last_move.index(oldest_move)] 47 | else: 48 | return None 49 | 50 | def solve(self): 51 | initial = time.time() 52 | self.initialize_pool() 53 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 54 | self.generate() 55 | self.initialize_cost() 56 | self.last_move = [-1 for _ in self.assignment] 57 | self.best_cost = len(self.id_unsat_clauses) 58 | self.tabu_tenure = int(self.nvars/10 + 4) 59 | ''' 60 | RoTS mechanism within MAX_FLIPS 61 | ''' 62 | while self.nb_flips < self.MAX_FLIPS and not self.check(): 63 | assert len(self.id_unsat_clauses) > 0 64 | ''' 65 | - GSAT idea (Intensification => focus on best var) 66 | - Among all variables that occur in unsat clauses 67 | - Choose a variable x which minimizes cost to flip 68 | ''' 69 | # compute allowed literals wrt tabu list 70 | all_allowed_lits, non_allowed_lits = self.pick_allowed_lits(self.id_unsat_clauses, tabu=True) 71 | if len(all_allowed_lits) == 0: # else take all_allowed_lits and ignore tabu 72 | all_allowed_lits, non_allowed_lits = self.pick_allowed_lits(self.id_unsat_clauses, tabu=False) 73 | ''' 74 | Compute cost of every (tabu and non tabu) moves 75 | Cost = break - make 76 | ''' 77 | ntb_cost, tb_cost = [], [] 78 | current_cost = len(self.id_unsat_clauses) 79 | for literal in all_allowed_lits: 80 | ntb_cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 81 | x_ntb = all_allowed_lits[np.argmin(ntb_cost)] 82 | for literal in non_allowed_lits: 83 | tb_cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 84 | if len(tb_cost) > 0: 85 | x_tb = non_allowed_lits[np.argmin(tb_cost)] 86 | if min(tb_cost) < min(ntb_cost) and current_cost + min(tb_cost) < self.best_cost: #EXCEPTION 87 | x = x_tb 88 | else: 89 | x = x_ntb 90 | else: 91 | x = x_ntb 92 | 93 | self.flip(x) 94 | self.last_move[abs(x)-1] = self.nb_flips 95 | if len(self.id_unsat_clauses) < self.best_cost: 96 | self.best_cost = len(self.id_unsat_clauses) 97 | ''' 98 | Every 10n iterations, if a variable is not flipped within 10n iterations 99 | => force X to be flipped ! 100 | ''' 101 | if self.nb_flips % self.CHECK_FREQ == 0: 102 | x = self.pick_necessary_flip() 103 | if x is not None: 104 | self.flip(x) 105 | self.last_move[abs(x)-1] = self.nb_flips 106 | if len(self.id_unsat_clauses) < self.best_cost: 107 | self.best_cost = len(self.id_unsat_clauses) 108 | ''' 109 | Every n iterations => change randomly tabu tenure 110 | ''' 111 | if self.nb_flips % self.nvars == 0: 112 | self.tabu_tenure = random.randint(self.tabu_tenure_MIN, self.tabu_tenure_MAX) 113 | if self.check(): 114 | self.is_sat = True 115 | 116 | end = time.time() 117 | print('Nb flips: {0} '.format(self.nb_flips)) 118 | print('Nb tries: {0} '.format(self.nb_tries)) 119 | print('CPU time: {0:10.4f} s '.format(end-initial)) 120 | if self.check(): 121 | print('SAT') 122 | return self.assignment 123 | else: 124 | print('UNKNOWN') 125 | return None 126 | 127 | 128 | -------------------------------------------------------------------------------- /full_basic_walksat_solver.py: -------------------------------------------------------------------------------- 1 | from dimacs_parser import parse 2 | import numpy as np 3 | import random 4 | import time 5 | 6 | class WalkSAT_Solver: 7 | 8 | def __init__(self, input_cnf_file, verbose): 9 | self.list_clauses, self.nvars = parse(input_cnf_file, verbose) 10 | self.verbose = verbose 11 | self.assignment = [] 12 | self.pool = dict() #key: literal -> element: index of clause which contains literal 13 | self.unsat_clauses = [] # save id of unsat clause 14 | self.costs = np.zeros(len(self.list_clauses)) #compute nb of literals make clause true (i.e. for clause Ci, if fi>0 => T, fi==0 => F) 15 | self.MAX_TRIES = 100 16 | self.MAX_FLIPS = 500 17 | self.nb_tries = 0 18 | self.nb_flips = 0 19 | self.is_sat = False 20 | self.noise_parameter = 0.2 21 | 22 | for clause in self.list_clauses: 23 | assert len(clause) > 0 24 | 25 | def generate(self): 26 | self.assignment = [] 27 | self.nb_tries += 1 28 | self.nb_flips = 0 29 | for x in range(1, self.nvars+1): 30 | choice = [-1,1] 31 | self.assignment.append(x * random.choice(choice)) 32 | 33 | def initialize_pool(self): 34 | for i, clause in enumerate(self.list_clauses): 35 | for literal in clause: 36 | if literal in self.pool.keys(): 37 | self.pool[literal].append(i) 38 | else: 39 | self.pool[literal] = [i] 40 | 41 | def initialize_cost(self): 42 | # Compute nb of literals make clause true (i.e. for clause Ci, if fi>0 => T, fi==0 => F) 43 | # Let's call it cost ! 44 | assert len(self.assignment) > 0 45 | self.unsat_clauses = [] 46 | for i, clause in enumerate(self.list_clauses): 47 | self.costs[i] = 0 48 | for literal in clause: 49 | if literal in self.assignment: 50 | self.costs[i] += 1 51 | if self.costs[i] == 0: #Clause[i] is currently UNSAT 52 | self.unsat_clauses.append(i) 53 | 54 | def check(self): 55 | # check if all is SAT 56 | return len(self.unsat_clauses) == 0 57 | 58 | def pick_unsat_clause(self): 59 | assert len(self.unsat_clauses) > 0 60 | random_index = random.choice(self.unsat_clauses) 61 | return self.list_clauses[random_index] 62 | 63 | def evaluate_breakcount(self, literal, bs=1, ms=1): 64 | # Compute the breakcount score: #clause which turn SAT -> UNSAT 65 | if literal in self.assignment: 66 | original_literal = literal 67 | elif -literal in self.assignment: 68 | original_literal = -literal 69 | # when flipping literal -> -literal 70 | # For every clause which contains literal => cost-- 71 | breakcount = 0 72 | if original_literal in self.pool.keys(): 73 | for i in self.pool[original_literal]: 74 | if self.costs[i] == 1: 75 | breakcount += 1 76 | # For every clause which contains -literal => cost ++ 77 | makecount = 0 78 | if -original_literal in self.pool.keys(): 79 | for j in self.pool[-original_literal]: 80 | if self.costs[j] == 0: 81 | makecount += 1 82 | # Score = break - make 83 | score = bs*breakcount - ms*makecount 84 | return score 85 | 86 | def flip(self, literal): 87 | self.nb_flips += 1 88 | # Flip variable in assignment 89 | if literal in self.assignment: 90 | ind = self.assignment.index(literal) 91 | elif -literal in self.assignment: 92 | ind = self.assignment.index(-literal) 93 | old_literal = self.assignment[ind] 94 | self.assignment[ind] *= -1 95 | # Update cost 96 | # Clause contains literal => cost -- 97 | if old_literal in self.pool.keys(): 98 | for i in self.pool[old_literal]: 99 | self.costs[i] -= 1 100 | if self.costs[i] == 0: # if SAT -> UNSAT: add to list of unsat clauses 101 | self.unsat_clauses.append(i) 102 | # Clause contains -literal => cost ++ 103 | if -old_literal in self.pool.keys(): 104 | for j in self.pool[-old_literal]: 105 | if self.costs[j] == 0: # if UNSAT -> SAT: remove from list of unsat clauses 106 | self.unsat_clauses.remove(j) 107 | self.costs[j] += 1 108 | 109 | def solve(self): 110 | initial = time.time() 111 | self.initialize_pool() 112 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 113 | self.generate() 114 | self.initialize_cost() 115 | while self.nb_flips < self.MAX_FLIPS and not self.is_sat: 116 | if self.check() == 1: # if no unsat clause => finish 117 | self.is_sat = True 118 | else: 119 | assert len(self.unsat_clauses) > 0 120 | # Choose a variable x to flip 121 | unsat_clause = self.pick_unsat_clause() 122 | break_count = [] 123 | for literal in unsat_clause: 124 | break_count.append(self.evaluate_breakcount(literal)) 125 | # if 0 in break_count: # that's an excellent x 126 | # x = unsat_clause[break_count.index(0)] 127 | # else: 128 | p = random.random() 129 | if p < self.noise_parameter: # pick x randomly from unsat clause 130 | x = random.choice(unsat_clause) 131 | else: 132 | x = unsat_clause[np.argmin(break_count)] 133 | self.flip(x) 134 | 135 | end = time.time() 136 | print('Nb flips: {0} '.format(self.nb_flips)) 137 | print('Nb tries: {0} '.format(self.nb_tries)) 138 | print('CPU time: {0:10.4f} s '.format(end-initial)) 139 | if self.is_sat: 140 | print('SAT') 141 | return self.assignment 142 | else: 143 | print('UNKNOWN') 144 | return None -------------------------------------------------------------------------------- /hamming_reactive_tabu_search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] R. Battiti and G. Tecchiolli, “The Reactive Tabu Search,” ORSA J. Comput., vol. 6, no. 2, pp. 126–140, 1994, doi: 10.1287/ijoc.6.2.126. 4 | [2] R. Battiti and M. Protasi, “Reactive Search, a history-based heuristic for MAX-SAT,” ACM J. Exp. Algorithmics, vol. 2, pp. 130–157, 1997, doi: 10.1145/264216.264220. 5 | ''' 6 | 7 | from base_solver import Base_Solver 8 | import numpy as np 9 | import random 10 | import time 11 | from itertools import chain 12 | 13 | class H_RTS(Base_Solver): 14 | 15 | def __init__(self, input_cnf_file, verbose): 16 | super(H_RTS, self).__init__(input_cnf_file, verbose) 17 | ''' 18 | Initialize tabu list and its length 19 | Note that tabu list is a circular list 20 | ''' 21 | self.tabu_list = [] 22 | self.Tf = 0.1 23 | self.tabu_tenure = 0 24 | 25 | def initialize_tabu(self, tabu_tenure): 26 | self.tabu_list = [] 27 | self.tabu_tenure = tabu_tenure 28 | 29 | def add_tabu(self, literal): 30 | ''' 31 | Add a move to tabu list 32 | ''' 33 | if len(self.tabu_list) < self.tabu_tenure: 34 | self.tabu_list.append(abs(literal)) 35 | else: # tabu list is full 36 | self.tabu_list.pop(0) 37 | self.tabu_list.append(literal) 38 | 39 | def hamming_distance(self,a, b): 40 | c = np.bitwise_xor(a, b) 41 | n = c.sum() 42 | return n 43 | 44 | def react(self, X_f, X_i): 45 | deriv = float(self.hamming_distance(X_f, X_i) / (self.tabu_tenure+1)) -1 46 | if deriv <= 0: 47 | self.Tf += 0.01 48 | elif deriv > 0.5: 49 | self.Tf -= 0.01 50 | if self.Tf > 0.25: 51 | self.Tf = 0.25 52 | elif self.Tf < 0.025: 53 | self.Tf = 0.025 54 | return max(int(self.Tf*self.nvars), 4) 55 | 56 | def pick_all_lits(self,id_unsat_clauses, tabu_list=None): 57 | all_allowed_lits = [] 58 | for ind in id_unsat_clauses: 59 | all_allowed_lits += self.list_clauses[ind] 60 | if tabu_list is not None: 61 | all_allowed_lits = list(set(all_allowed_lits)^set(tabu_list)) 62 | else: 63 | all_allowed_lits = list(set(all_allowed_lits)) 64 | return all_allowed_lits 65 | 66 | def solve(self): 67 | initial = time.time() 68 | self.initialize_pool() 69 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 70 | self.generate() 71 | self.initialize_cost() 72 | self.Tf = 0.1 73 | self.tabu_tenure = int(self.Tf * self.nvars) 74 | ''' 75 | TODO: NOB_LS here 76 | ''' 77 | while self.nb_flips < self.MAX_FLIPS and not self.check(): 78 | ''' 79 | [Local Search] 80 | - GSAT idea (Intensification => focus on best var) 81 | - Among all variables that occur in unsat clauses 82 | - Choose a variable x which minimizes cost to flip 83 | - Compute cost when flipping each literal 84 | - Cost = break - make 85 | - After flipping, check if the nb of UNSAT clause decreases or not 86 | - Yes => continuer 87 | - No => stop at local optimum 88 | ''' 89 | improved = True 90 | while improved and self.nb_flips < self.MAX_FLIPS and not self.check(): 91 | all_allowed_lits = self.pick_all_lits(self.id_unsat_clauses) 92 | break_make_count = [] 93 | for literal in all_allowed_lits: 94 | break_make_count.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 95 | nb_unsat = len(self.id_unsat_clauses) 96 | if nb_unsat + min(break_make_count) < nb_unsat: 97 | x = all_allowed_lits[np.argmin(break_make_count)] 98 | self.flip(x) 99 | else: 100 | improved = False 101 | X_i = self.assignment.copy() 102 | ''' 103 | [Reactive Tabu Search] 104 | - Initialize tabu list with defined tabu tenure 105 | - Compute new X after 2(T+1) iterations with TS 106 | + Remove all tabus in list of candidates 107 | + If all vars are tabu => ignore tabu and take initial list of variables 108 | - Update tabu tenure 109 | ''' 110 | it = 0 111 | while not self.check() and it < 2*(self.tabu_tenure+1): 112 | # compute allowed literals wrt tabu list 113 | all_allowed_lits = self.pick_all_lits(self.id_unsat_clauses, self.tabu_list) 114 | if len(all_allowed_lits) == 0: # else take all_allowed_lits and ignore tabu 115 | all_allowed_lits = self.pick_all_lits(self.id_unsat_clauses) 116 | break_make_count = [] 117 | for literal in all_allowed_lits: 118 | break_make_count.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 119 | x = all_allowed_lits[np.argmin(break_make_count)] 120 | # ''' 121 | # Random walk 122 | # ''' 123 | # if self.random_walk: 124 | # p = random.random() 125 | # if p < self.noise_parameter: # pick x randomly from literals in all unsat clause 126 | # x = random.choice(all_allowed_lits) 127 | # else: 128 | # x = all_allowed_lits[np.argmin(break_count)] 129 | # else: 130 | # x = all_allowed_lits[np.argmin(break_count)] 131 | self.flip(x) 132 | self.add_tabu(x) 133 | it += 1 134 | X_f = self.assignment.copy() 135 | ''' 136 | Update tabu tenure based on search history 137 | ''' 138 | self.tabu_tenure = self.react(X_f, X_i) 139 | if self.check(): 140 | self.is_sat = True 141 | end = time.time() 142 | print('Nb flips: {0} '.format(self.nb_flips)) 143 | print('Nb tries: {0} '.format(self.nb_tries)) 144 | print('CPU time: {0:10.4f} s '.format(end-initial)) 145 | if self.is_sat: 146 | print('SAT') 147 | return self.assignment 148 | else: 149 | print('UNKNOWN') 150 | return None 151 | 152 | 153 | -------------------------------------------------------------------------------- /r_novelty.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] D. McAllester, B. Selman, and H. Kautz, “Evidence for invariants in local search,” Proc. Natl. Conf. Artif. Intell., pp. 321–326, 1997. 4 | [2] H. H. Hoos and T. Stützle, “Towards a characterization of the behaviour of stochastic local search algorithms for SAT,” Artif. Intell., vol. 112, no. 1, pp. 213–232, 1999, doi: 10.1016/S0004-3702(99)00048-X. 5 | ''' 6 | 7 | from base_solver import Base_Solver 8 | import numpy as np 9 | import random 10 | import time 11 | from itertools import chain 12 | 13 | class R_Novelty(Base_Solver): 14 | 15 | def __init__(self, input_cnf_file, verbose, noise_parameter = 0.2, random_walk_noise = None): 16 | super(R_Novelty, self).__init__(input_cnf_file, verbose) 17 | self.noise_parameter = noise_parameter 18 | self.most_recent = None 19 | # Introduce random walk noise parameter => Novelty+ 20 | self.random_walk_noise = random_walk_noise 21 | 22 | def solve(self): 23 | initial = time.time() 24 | self.initialize_pool() 25 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 26 | self.generate() 27 | self.initialize_cost() 28 | self.most_recent = None 29 | while self.nb_flips < self.MAX_FLIPS and not self.is_sat: 30 | if self.check() == 1: # if no unsat clause => finish 31 | self.is_sat = True 32 | else: 33 | assert len(self.id_unsat_clauses) > 0 34 | ''' 35 | - GSAT idea (Intensification => focus on best var) 36 | - Among all variables that occur in unsat clauses 37 | - Choose a variable x which minimizes cost to flip 38 | ''' 39 | all_unsat_lits = [] 40 | for ind in self.id_unsat_clauses: 41 | all_unsat_lits += self.list_clauses[ind] 42 | all_unsat_lits = list(set(all_unsat_lits)) # flatten & remove redundants 43 | ''' 44 | Compute cost when flipping each literal 45 | cost = break - make 46 | Best var with min cost 47 | ''' 48 | cost = [] 49 | for literal in all_unsat_lits: 50 | cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 51 | ''' 52 | Random walk 53 | ''' 54 | apply_r_novelty = True 55 | if self.random_walk_noise is not None: 56 | wp = random.random() 57 | if wp < self.random_walk_noise: 58 | x = random.choice(all_unsat_lits) 59 | apply_r_novelty = False 60 | ''' 61 | [R_Novelty strategy] 62 | Arrange variables according to each cost 63 | (1) If the best one *x1* is NOT the most recently flipped variable => select *x1*. 64 | Otherwise, let n = |cost(x1) - cost(x2)| >= 1. Then if: 65 | (2a) p < 0.5 & n > 1 => pick *x1* 66 | 67 | (2b) p < 0.5 & n = 1 => pick *x2* with probability 2p, otherwise *x1* 68 | 69 | (2c) p >= 0.5 & n = 1 => pick *x2* 70 | 71 | (2d) p >= 0.5 & n > 1 => pick *x2* with probability 2(p-0.5), otherwise *x1* 72 | ''' 73 | if apply_r_novelty: 74 | if len(all_unsat_lits) == 1: 75 | x = all_unsat_lits[0] 76 | else: 77 | best_id = np.argmin(cost) 78 | best_cost = cost[best_id] 79 | best_var = all_unsat_lits[best_id] 80 | ## Need to find second best cost != best cost 81 | second_best_id, second_best_cost, second_best_var = best_id, best_cost, best_var 82 | while second_best_cost == best_cost and len(cost) > 1: 83 | cost.pop(second_best_id) 84 | all_unsat_lits.remove(second_best_var) 85 | second_best_id= np.argmin(cost) 86 | second_best_cost = cost[second_best_id] 87 | second_best_var = all_unsat_lits[second_best_id] 88 | # best_var, second_best_var = all_unsat_lits[0], all_unsat_lits[1] 89 | if abs(best_var) != self.most_recent: #(1) 90 | x = best_var 91 | else: 92 | n = abs(best_cost - second_best_cost) 93 | p = random.random() 94 | if n == 0: # all variables has the same cost => pick randomly as Novelty 95 | if p < self.noise_parameter: #(2a) 96 | x = second_best_var 97 | else: #(2b) 98 | x = best_var 99 | else: 100 | ''' 101 | R-Novelty's core idea, with n>=1 102 | ''' 103 | if self.noise_parameter < 0.5 and n > 1: 104 | x = best_var 105 | elif self.noise_parameter < 0.5 and n == 1: 106 | if p < 2*self.noise_parameter: 107 | x = second_best_var 108 | else: 109 | x = best_var 110 | elif self.noise_parameter >= 0.5 and n == 1: 111 | x = second_best_var 112 | elif self.noise_parameter >= 0.5 and n > 1: 113 | if p < 2*(self.noise_parameter-0.5): 114 | x = second_best_var 115 | else: 116 | x = best_var 117 | ''' 118 | After choosing x, flip it and save this last recent move 119 | ''' 120 | self.flip(x) 121 | self.most_recent = abs(x) 122 | 123 | end = time.time() 124 | print('Nb flips: {0} '.format(self.nb_flips)) 125 | print('Nb tries: {0} '.format(self.nb_tries)) 126 | print('CPU time: {0:10.4f} s '.format(end-initial)) 127 | if self.is_sat: 128 | print('SAT') 129 | return self.assignment 130 | else: 131 | print('UNKNOWN') 132 | return None 133 | 134 | 135 | -------------------------------------------------------------------------------- /adaptive_novelty.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] D. McAllester, B. Selman, and H. Kautz, “Evidence for invariants in local search,” Proc. Natl. Conf. Artif. Intell., pp. 321–326, 1997. 4 | [2] H. H. Hoos and T. Stützle, “Towards a characterization of the behaviour of stochastic local search algorithms for SAT,” Artif. Intell., vol. 112, no. 1, pp. 213–232, 1999, doi: 10.1016/S0004-3702(99)00048-X. 5 | [3] H. H. Hoos, “An adaptive noise mechanism for walkSAT,” Proc. Natl. Conf. Artif. Intell., pp. 655–660, 2002. 6 | ''' 7 | 8 | from base_solver import Base_Solver 9 | import numpy as np 10 | import random 11 | import time 12 | from itertools import chain 13 | 14 | class Adaptive_Novelty(Base_Solver): 15 | 16 | def __init__(self, input_cnf_file, verbose, noise_parameter = 0.2): 17 | super(Adaptive_Novelty, self).__init__(input_cnf_file, verbose) 18 | self.noise_parameter = noise_parameter 19 | self.most_recent = None 20 | ''' 21 | Introduce random walk noise parameter => Adaptive_Novelty+ 22 | Initially set noise parameter = 0 23 | Then dynamically adjust noise for escaping the stagnation 24 | Adjustment based on parameters theta and phi 25 | ''' 26 | self.random_walk_noise = 0.0 27 | self.THETA = float(1/6) 28 | self.DEFINED_STEP = self.THETA * len(self.list_clauses) 29 | self.PHI = 0.2 30 | self.no_improvement_step = 0 31 | self.stagnation = False 32 | 33 | def solve(self): 34 | initial = time.time() 35 | self.initialize_pool() 36 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 37 | self.generate() 38 | self.initialize_cost() 39 | self.most_recent = None 40 | self.no_improvement_step = 0 41 | self.stagnation = False 42 | self.random_walk_noise = 0.0 43 | while self.nb_flips < self.MAX_FLIPS and not self.is_sat: 44 | if self.check() == 1: # if no unsat clause => finish 45 | self.is_sat = True 46 | else: 47 | assert len(self.id_unsat_clauses) > 0 48 | ''' 49 | - [GSAT] idea (Intensification => focus on best var) 50 | - Among all variables that occur in unsat clauses 51 | - Choose a variable x which minimizes cost to flip 52 | ''' 53 | all_unsat_lits = [] 54 | for ind in self.id_unsat_clauses: 55 | all_unsat_lits += self.list_clauses[ind] 56 | all_unsat_lits = list(set(all_unsat_lits)) # flatten & remove redundants 57 | ''' 58 | Compute cost when flipping each literal 59 | cost = break - make 60 | Best var with min cost 61 | ''' 62 | cost = [] 63 | for literal in all_unsat_lits: 64 | cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 65 | ''' 66 | [Random walk] 67 | If after DEFINED_STEP, no improvements => increase random_walk_noise 68 | Over come stagnation => decrese random_walk_noise 69 | ''' 70 | apply_novelty = True 71 | if self.stagnation: 72 | # Stagnation found => count ! 73 | self.no_improvement_step += 1 74 | if self.no_improvement_step >= self.DEFINED_STEP: 75 | # Increase noise after a defined number of no-improved steps 76 | self.random_walk_noise += float((1-self.random_walk_noise) * self.PHI) 77 | # Reset to check if it can overcome the stagnation within next DEFINED_STEP 78 | self.no_improvement_step = 0 79 | else: 80 | # No stagnation => decrease noise ! 81 | self.random_walk_noise -= float(self.random_walk_noise * 2 * self.PHI) 82 | self.no_improvement_step = 0 83 | # Random walk to overcome stagnations 84 | if self.random_walk_noise > 0: 85 | wp = random.random() 86 | if wp < self.random_walk_noise: 87 | x = random.choice(all_unsat_lits) 88 | apply_novelty = False 89 | ''' 90 | [Novelty strategy] 91 | Arrange variables according to each cost 92 | (1) If the best one *x1* is NOT the most recently flipped variable => select *x1*. 93 | Otherwise, 94 | (2a) select *x2* with probability p, 95 | (2b) select *x1* with probability 1-p. 96 | ''' 97 | if apply_novelty: 98 | if len(all_unsat_lits) == 1: 99 | x = all_unsat_lits[0] 100 | else: 101 | best_id = np.argmin(cost) 102 | best_var = all_unsat_lits[best_id] 103 | cost.pop(best_id) 104 | all_unsat_lits.remove(best_var) 105 | second_best_id = np.argmin(cost) 106 | second_best_var = all_unsat_lits[second_best_id] 107 | # best_var, second_best_var = all_unsat_lits[0], all_unsat_lits[1] 108 | if abs(best_var) != self.most_recent: #(1) 109 | x = best_var 110 | else: 111 | p = random.random() 112 | if p < self.noise_parameter: #(2a) 113 | x = second_best_var 114 | else: #(2b) 115 | x = best_var 116 | ''' 117 | After choosing x, flip it and save this last recent move 118 | Compare new cost with previous one => check number of no-improvement step 119 | ''' 120 | self.current_cost = len(self.id_unsat_clauses) 121 | self.flip(x) 122 | self.most_recent = abs(x) 123 | if len(self.id_unsat_clauses) >= self.current_cost: 124 | self.stagnation = True 125 | else: 126 | self.stagnation = False 127 | 128 | end = time.time() 129 | print('Nb flips: {0} '.format(self.nb_flips)) 130 | print('Nb tries: {0} '.format(self.nb_tries)) 131 | print('CPU time: {0:10.4f} s '.format(end-initial)) 132 | if self.is_sat: 133 | print('SAT') 134 | return self.assignment 135 | else: 136 | print('UNKNOWN') 137 | return None 138 | 139 | 140 | -------------------------------------------------------------------------------- /iterated_robust_tabu_search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] K. Smyth, H. H. Hoos, and T. Stützle, “Iterated robust tabu search for MAX-SAT,” in Lecture Notes in Computer Science (including subseries Lecture Notes in Artificial Intelligence and Lecture Notes in Bioinformatics), 2003, vol. 2671, pp. 129–144, doi: 10.1007/3-540-44886-1_12. 4 | ''' 5 | 6 | from base_solver import Base_Solver 7 | import numpy as np 8 | import random 9 | import time 10 | from itertools import chain 11 | 12 | class IRoTS(Base_Solver): 13 | 14 | def __init__(self, input_cnf_file, verbose): 15 | super(IRoTS, self).__init__(input_cnf_file, verbose) 16 | ''' 17 | Instead of using a circular list, use a list for tracking last move of each variable 18 | If current_time - last_move < tabu_tenure => a tabu move ! 19 | Else => non-tabu moves 20 | Initialize all by -1 21 | ''' 22 | self.tabu_tenure_LS = int(self.nvars/10 + 4) 23 | # self.tabu_tenure_LS_MIN = int(self.nvars/10) 24 | # self.tabu_tenure_LS_MAX = int(self.nvars/10) * 3 25 | self.tabu_tenure_Perturb = int(self.nvars/2) 26 | self.last_move = [-1 for _ in self.assignment] 27 | self.best_cost = len(self.list_clauses) 28 | self.CHECK_FREQ = self.nvars * 10 29 | self.nb_no_improvements = 0 30 | self.ESCAPE_THRESHOLD = int(self.nvars*self.nvars/4) 31 | self.nb_perturbations = 0 32 | self.MAX_PERTURBATIONS = int(9*self.nvars/10) 33 | 34 | def pick_allowed_lits(self,id_unsat_clauses, tabu_tenure): 35 | all_allowed_lits = [] 36 | non_allowed_lits = [] 37 | for ind in id_unsat_clauses: 38 | all_allowed_lits += self.list_clauses[ind] 39 | all_allowed_lits = list(set(all_allowed_lits)) 40 | if tabu_tenure > 0: 41 | for lit in all_allowed_lits: 42 | if self.nb_flips - self.last_move[abs(lit)-1] < tabu_tenure: #tabu move 43 | all_allowed_lits.remove(lit) 44 | non_allowed_lits.append(lit) 45 | return all_allowed_lits, non_allowed_lits 46 | 47 | def pick_necessary_flip(self): 48 | oldest_move = min(self.last_move) 49 | if self.nb_flips - oldest_move > self.CHECK_FREQ: 50 | return self.assignment[self.last_move.index(oldest_move)] 51 | else: 52 | return None 53 | 54 | def RoTS(self, mode_LS=False, mode_Perturbation=False): 55 | condition = False 56 | self.nb_perturbations = 0 57 | self.nb_no_improvements = 0 58 | if mode_LS: 59 | condition = self.nb_no_improvements < self.ESCAPE_THRESHOLD 60 | tabu_tenure = self.tabu_tenure_LS 61 | elif mode_Perturbation: 62 | condition = self.nb_perturbations < self.MAX_PERTURBATIONS 63 | tabu_tenure = self.tabu_tenure_Perturb 64 | 65 | while condition and self.nb_flips < self.MAX_FLIPS and not self.check() : 66 | ''' 67 | compute allowed literals wrt tabu list 68 | ''' 69 | all_allowed_lits, non_allowed_lits = self.pick_allowed_lits(self.id_unsat_clauses, tabu_tenure) 70 | if len(all_allowed_lits) == 0: # else take all_allowed_lits and ignore tabu 71 | all_allowed_lits, non_allowed_lits = self.pick_allowed_lits(self.id_unsat_clauses, 0) 72 | ''' 73 | Compute cost of every (tabu and non tabu) moves 74 | Cost = break - make 75 | ''' 76 | ntb_cost, tb_cost = [], [] 77 | current_cost = len(self.id_unsat_clauses) 78 | for literal in all_allowed_lits: 79 | ntb_cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 80 | x_ntb = all_allowed_lits[np.argmin(ntb_cost)] 81 | for literal in non_allowed_lits: 82 | tb_cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 83 | if len(tb_cost) > 0: 84 | x_tb = non_allowed_lits[np.argmin(tb_cost)] 85 | if min(tb_cost) < min(ntb_cost) and current_cost + min(tb_cost) < self.best_cost: #EXCEPTION 86 | x = x_tb 87 | else: 88 | x = x_ntb 89 | else: 90 | x = x_ntb 91 | 92 | self.flip(x) 93 | self.last_move[abs(x)-1] = self.nb_flips 94 | if len(self.id_unsat_clauses) < self.best_cost: 95 | self.best_cost = len(self.id_unsat_clauses) 96 | self.nb_no_improvements = 0 97 | else: 98 | self.nb_no_improvements += 1 99 | ''' 100 | Every 10n iterations, if a variable is not flipped within 10n iterations 101 | => force X to be flipped ! 102 | ''' 103 | if self.nb_flips % self.CHECK_FREQ == 0: 104 | x = self.pick_necessary_flip() 105 | if x is not None: 106 | self.flip(x) 107 | self.last_move[abs(x)-1] = self.nb_flips 108 | if len(self.id_unsat_clauses) < self.best_cost: 109 | self.best_cost = len(self.id_unsat_clauses) 110 | self.nb_no_improvements = 0 111 | else: 112 | self.nb_no_improvements += 1 113 | ''' 114 | TODO: Every n iterations => change randomly tabu tenure 115 | Note: tabu tenure for perturbation phase should be larger than the one used for LS 116 | ''' 117 | # if self.nb_flips % self.nvars == 0: 118 | # self.tabu_tenure = random.randint(self.tabu_tenure_MIN, self.tabu_tenure_MAX) 119 | self.nb_perturbations += 1 120 | if mode_LS: 121 | condition = self.nb_no_improvements < self.ESCAPE_THRESHOLD 122 | elif mode_Perturbation: 123 | condition = self.nb_perturbations < self.MAX_PERTURBATIONS 124 | return self.check() 125 | 126 | def solve(self): 127 | initial = time.time() 128 | self.initialize_pool() 129 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 130 | ''' 131 | Random assignment & parameter initialization 132 | ''' 133 | self.generate() 134 | self.initialize_cost() 135 | self.last_move = [-1 for _ in self.assignment] 136 | self.best_cost = len(self.id_unsat_clauses) 137 | # self.tabu_tenure_LS = int(self.nvars/10 + 4) 138 | # self.tabu_tenure_Perturb = int(self.nvars/2) 139 | ''' 140 | LS 141 | ''' 142 | self.is_sat = self.RoTS(mode_LS=True) 143 | while not self.is_sat and self.nb_flips < self.MAX_FLIPS: 144 | ''' 145 | Pertubation Operator 146 | ''' 147 | x_star = self.assignment.copy() 148 | x_star_cost = len(self.id_unsat_clauses) 149 | self.last_move = [-1 for _ in self.assignment] 150 | self.is_sat = self.RoTS(mode_Perturbation=True) 151 | ''' 152 | LS 153 | ''' 154 | xp_star = self.assignment.copy() 155 | xp_star_cost = len(self.id_unsat_clauses) 156 | self.last_move = [-1 for _ in self.assignment] 157 | if not self.is_sat: 158 | self.is_sat = self.RoTS(mode_LS=True) 159 | xp_star = self.assignment.copy() 160 | xp_star_cost = len(self.id_unsat_clauses) 161 | ''' 162 | Acceptance Criterion 163 | ''' 164 | if xp_star_cost < self.best_cost: 165 | self.best_cost = xp_star_cost 166 | self.assignment = xp_star 167 | else: 168 | p = random.random() 169 | if xp_star_cost == x_star_cost: 170 | if p < 0.5: 171 | self.assignment = xp_star 172 | else: 173 | self.assignment = x_star 174 | elif xp_star_cost > x_star_cost: 175 | if p < 0.1: 176 | self.assignment = x_star 177 | else: 178 | self.assignment = xp_star 179 | elif xp_star_cost < x_star_cost: 180 | if p < 0.1: 181 | self.assignment = xp_star 182 | else: 183 | self.assignment = x_star 184 | self.initialize_cost() 185 | 186 | 187 | end = time.time() 188 | print('Nb flips: {0} '.format(self.nb_flips)) 189 | print('Nb tries: {0} '.format(self.nb_tries)) 190 | print('CPU time: {0:10.4f} s '.format(end-initial)) 191 | if self.is_sat: 192 | print('SAT') 193 | return self.assignment 194 | else: 195 | print('UNKNOWN') 196 | return None 197 | 198 | 199 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Local Search based algorithms for solving SAT 2 | 3 | This repo is an (re)implementation of LS-based SAT Solver, in which a typical algorithm is WalkSAT. 4 | Let's start !! 5 | 6 | ## Core functions 7 | 8 | Some core functions of this solver are: 9 | 10 | - [X] Generator => generate randomly a truth function 11 | - [X] Checker => verify if assignment satisfies the formula 12 | - [X] Detector => pick an (or all) UNSAT clause(s) 13 | - [X] Evaluator => compute the break-count when flipping a variable 14 | - [X] Stopping criterion 15 | 16 | ***Comment:*** In my opinion, implementation of WalkSAT is much more easier than CDCL-based solver :) 17 | 18 | ## General pipeline 19 | 20 | The main pipeline is: 21 | 22 | 1- Generate an assignment. 23 | 24 | 2- Check if SAT or not. If SAT => Done. 25 | 26 | 3- If UNSAT, pick (intelligently or magically) 'best' variable to flip. Repeat 2. 27 | 28 | ## Analytics 29 | 30 | The algorithm only stops when formula is SAT or after a defined number of tries and flips. That's why the result is either SAT or UNKNOWN -> not sure if the instances are UNSAT-. 31 | 32 | The main computational cost is of *Checker* part: check if assignment satisfies formula, i.e. **X |= F**?. 33 | 34 | Besides, assignment **X' = flip(X)** => the difference is only of one variable => how can we save computational cost in (re)checking **X' |= F**?. 35 | 36 | In addition, how can we compute efficiently break-count and make-count of a variable *x*?. 37 | 38 | Which variable should be flipped?. 39 | 40 | How to escape the stagnation situation?. 41 | 42 | etc. 43 | 44 | ## Implementations of LS-based variants 45 | 46 | #### 1. GSAT, 1992 :white_check_mark: 47 | 48 | ***Idea:*** Considering all variables in all unsat clauses, then pick the one that minimizes the number of UNSAT clauses => min cost = break - make. 49 | 50 | #### 2. GSAT/Random Walk, 1993 :white_check_mark: 51 | 52 | ***Idea:*** With probability p => pick any variable. Otherwise => pick best local move !. 53 | 54 | #### 3. WalkSAT & WalkSAT/Random Walk, 1994 :white_check_mark: 55 | 56 | ***Idea:*** Pick (randomly) one UNSAT clause, then pick the variable that minimizes break => score = break. In addition, the original strategy of Selman, Kautz, and Cohen (1994), called SKC strategy, proposed that: "never make a random move if there exists one literal with zero break-count". Obviously, flipping literal with zero break-count will improve the objective function (= number of SAT clauses). 57 | 58 | #### 4. GSAT/Tabu & WalkSAT/Tabu, 1997 :white_check_mark: 59 | 60 | ***Idea:*** Use a tabu list which store (flipped variables, tabu tenures) to avoid repeating these last recent moves. Intuitively, a couple *(x,t)* means that we forbid flipping *x* for next *t* iterations ! More concretely, 61 | 62 | - Refuse flipping a variable in tabu list 63 | 64 | - In case of *WalkSAT/Tabu*, if all the variables in the chosen UNSAT clause are tabu => choose another UNSAT clause instead 65 | 66 | - If all variables in all UNSAT clauses are tabus => tabu list is temporarily ignored 67 | 68 | In general, tabu list should be implemented as a FIFO circular list => tabu tenure *t* is fixed (i.e. the length of tabu list) during the search. However, tabu tenure can also be dynamically changed during search in a more complex variant (Reactive Tabu Search, we will see it later). 69 | 70 | 71 | #### 5. Hamming-Reactive Tabu Search (H-RTS), 1997 :white_check_mark: 72 | 73 | ***Idea:*** Tabu tenure T(t) is dynamically changed during the search. More precisely, "T(t) increases when repetitions happen and decreases when repetitions disappear for a sufficiently long search period". The general pipeline proposed by R.Battiti and M.Prostasi (1997) is: 74 | 75 | ```python 76 | while stop_trying_criterion is not satisfied: 77 | X = random_assignment 78 | Tf = 0.1 # fractional prohibition 79 | T = int(Tf * nvars) # tabu tenure 80 | ''' 81 | Non-oblivious LS => find quickly a local optimum 82 | NOB-LS is similar to OLS, except the objective function used to choose the best variable to flip 83 | ''' 84 | X = NOB_LS(f_NOB) 85 | 86 | while stop_flipping_criterion is not satisfied: 87 | ''' 88 | Oblivious local search => find a local optimum 89 | Put simply, use cost = break-make = f_OB 90 | ''' 91 | X = OLS(f_OB) 92 | X_I = X 93 | ''' 94 | Reactive Tabu Search 95 | - Compute X after 2(T+1) iterations 96 | - Change dynamically T 97 | ''' 98 | for 2(T+1) iterations: 99 | X = TabuSearch(f_OB) 100 | X_F = X 101 | T = react(Tf, X_F, X_I) 102 | ``` 103 | 104 | Note: In fact, the choice of non-oblivious objective function is a challenge w.r.t. different instances. That's why in this implementation, I only use OLS function for finding local the optimum, but theoretically, a NOB should be implemented. 105 | 106 | ***Reactive search => Reinforcement learning for heuristics*** 107 | 108 | #### 6. Novelty, 1997 :white_check_mark: 109 | 110 | ***Idea:*** Sort variables according to its cost, as does GSAT. Under this specific sort, consider the best *x1* and second-best variable *x2*. 111 | 112 | (1) If the best one *x1* is NOT the most recently flipped variable => select *x1*. 113 | 114 | Otherwise, (2a) select *x2* with probability p, (2b) select *x1* with probability 1-p. 115 | 116 | => Improvements for selecting flipping variables by breaking tie in favor of the least recently variable. 117 | 118 | => Enhance its diversification capacity. 119 | 120 | #### 7. R-Novelty, 1997 :white_check_mark: 121 | 122 | ***Idea:*** Similar to Novelty, except in case of *x1* is the most recently flipped variable !. 123 | 124 | In this case, let n = |cost(x1) - cost(x2)| >= 1. Then if: 125 | 126 | (2a) p < 0.5 & n > 1 => pick *x1* 127 | 128 | (2b) p < 0.5 & n = 1 => pick *x2* with probability 2p, otherwise *x1* 129 | 130 | (2c) p >= 0.5 & n = 1 => pick *x2* 131 | 132 | (2d) p >= 0.5 & n > 1 => pick *x2* with probability 2(p-0.5), otherwise *x1* 133 | 134 | Intuitively, the idea behind R_Novelty is that the difference in objective function should influence our choice, i.e. a large difference favors the best one. 135 | 136 | - [ ] Influence of noise parameter p ? 137 | 138 | #### 8. Novelty+ & R-Novelty+, 1999 :white_check_mark: 139 | 140 | ***Idea:*** Introduce Random Walk into Novelty and R-Novelty to prevent the extreme stagnation behavior. With probability wp => pick randomly, otherwise with probability 1-wp, follow the strategy of Novelty and R-Novelty. 141 | 142 | #### 9. AdaptNovelty+, 2002 :white_check_mark: 143 | 144 | ***Idea:*** Adjust the noise parameter wp according to search history, i.e. increase wp when detecting a stagnation behavior. Then decrease wp until the next stagnation situation is detected. Concretely, 145 | 146 | - Initialize wp = 0 => greedy search with Novelty strategy 147 | 148 | - No improvements over some predefined steps => stagnation detected 149 | 150 | - Increase wp until quit stagnation situation 151 | 152 | - Overcome the stagnation => decrease wp until the next stagnation is detected 153 | 154 | Note : dynamic noise parameter of random walk, not the one of Novelty mechanism. 155 | 156 | #### 10. Robust Tabu Search (RoTS), 1991 :white_check_mark: 157 | 158 | ***Idea:*** Similar with traditional TS, but with an exception, called as *aspiration mechanism* : if a tabu moves can lead to an improvement (better than non-tabu moves) over the best solution seen so far, the tabu move is accepted ! 159 | 160 | - In addition, if a variable is not flipped within long steps => it is forced to be flipped. 161 | 162 | - Finally, randomize tabu tenure every n iterations. 163 | 164 | #### 11. Iterated Robust Tabu Search (IRoTS), 2003 :white_check_mark: 165 | 166 | ***Idea:*** Combine the performances of both Iterated LS and TS. The core pipeline is based on Iterated Local Search, in which the pseudo code is shown below: 167 | 168 | ```python 169 | x0 = random_assignment 170 | x* = LocalSearch(x0) ## --> RoTS here! 171 | 172 | while stopping_criterion == False: 173 | x_p = Perturbation(x*) ## --> RoTS here! 174 | x_p* = LocalSearch(x_p) ## --> RoTS here! 175 | x* = AcceptanceCriterion(x*, X_p*) 176 | ``` 177 | 178 | Simply by involving LocalSearch and Perturbation procedure based on RoTS => Iterated RoTS (IRoTS). Note that the tabu tenure used for Perturbation phase is substantially larger than the one used for LS phase => favor the diversification ! On the other hands, the number of RoTS iterations in LS phase is much more larger than the one in Perturbation phase (i.e escape_threshold of LS >> perturbation_iterations) => favor the intensification ! 179 | 180 | #### 12. Adaptive Memory-Based Local Search (AMLS), 2012 :white_check_mark: 181 | 182 | ***Idea:*** Combine the stategies of aformentioned heuristics [4]. 183 | 184 | ## Result and comparation of different strategies 185 | 186 | Let's review some strategies of LS-based SAT Solver by fixing some parameters (MAX_FLIPS = 500, MAX_TRIES = 100, noise_parameter = 0.2) and compare their performance with only medium **SAT instances** (*e.g. uf-20-0x.cnf or uf-50-0x.cnf*). As aforementioned, given UNSAT instances, the results are UNKNOWN. 187 | 188 | In order to have a better overview and best comparison of these strategies, we should run tests on MaxSAT problems !!! 189 | 190 | ## TODO 191 | 192 | - [ ] Use 2-flip or 3-flip neighborhoods instead of 1-flip ones 193 | 194 | - [ ] Implement other heuristics for choosing unsat clause and variable to flip ! 195 | 196 | - [ ] Use a cache for storing score of every variable 197 | 198 | - [ ] Find benchmarking dataset (e.g. [SATLIB benchmark](https://www.cs.ubc.ca/~hoos/SATLIB/benchm.html)) and criterions for measuring the performance of a strategy and use it to compare with others 199 | 200 | - [ ] Build a test script for comparing performances of all implemented strategies 201 | 202 | - [ ] Further idea is to apply Knowledge Compilation techniques so that we can answer consistence query in polynomial time 203 | 204 | - [ ] Involve to Max-SAT & finding Max-SAT benchmarking instances 205 | 206 | ## References 207 | 208 | - [1] B. Selman, H. Kautz, and B. Cohen, “Noise strategies for local search,” AAAI/IAAI Proc., no. 1990, pp. 337–343, 1994. 209 | 210 | - [2] E. Taillard, “Robust taboo search for the quadratic assignment problem,” Parallel Comput., vol. 17, no. 4–5, pp. 443–455, 1991, doi: 10.1016/S0167-8191(05)80147-4. 211 | 212 | - [3] K. Smyth, H. H. Hoos, and T. Stützle, “Iterated robust tabu search for MAX-SAT,” in Lecture Notes in Computer Science (including subseries Lecture Notes in Artificial Intelligence and Lecture Notes in Bioinformatics), 2003, vol. 2671, pp. 129–144, doi: 10.1007/3-540-44886-1_12. 213 | 214 | - [4] Z. Lü and J.-K. K. Hao, “Adaptive Memory-Based Local Search for MAX-SAT,” Elsevier, Aug. 2012. doi: 10.1016/j.asoc.2012.01.013. 215 | 216 | - [5] M. Yagiura and T. Ibaraki, “Efficient 2 and 3-flip neighborhood search algorithms for the MAX SAT,” Lect. Notes Comput. Sci. (including Subser. Lect. Notes Artif. Intell. Lect. Notes Bioinformatics), vol. 1449, no. 2, pp. 105–116, 1998, doi: 10.1007/3-540-68535-9_14. 217 | 218 | - [6] D. Pankratov and A. Borodin, “On the relative merits of simple local search methods for the MAX-SAT problem,” Lect. Notes Comput. Sci. (including Subser. Lect. Notes Artif. Intell. Lect. Notes Bioinformatics), vol. 6175 LNCS, pp. 223–236, 2010, doi: 10.1007/978-3-642-14186-7_19. 219 | 220 | - [7] R. Battiti and M. Protasi, “Reactive Search, a history-sensitive heuristic for MAX-SAT,” ACM J. Exp. Algorithmics, vol. 2, p. 2, 1997, doi: 10.1145/264216.264220. 221 | 222 | - [8] R. Battiti and G. Tecchiolli, “The Reactive Tabu Search,” ORSA J. Comput., vol. 6, no. 2, pp. 126–140, 1994, doi: 10.1287/ijoc.6.2.126. 223 | 224 | - [9] H. H. Hoos, “An adaptive noise mechanism for walkSAT,” Proc. Natl. Conf. Artif. Intell., pp. 655–660, 2002. 225 | 226 | - [10] H. H. Hoos and T. Stützle, “Towards a characterization of the behaviour of stochastic local search algorithms for SAT,” Artif. Intell., vol. 112, no. 1, pp. 213–232, 1999, doi: 10.1016/S0004-3702(99)00048-X. 227 | 228 | - [11] D. McAllester, B. Selman, and H. Kautz, “Evidence for invariants in local search,” Proc. Natl. Conf. Artif. Intell., pp. 321–326, 1997. 229 | 230 | - [12] B. Mazure, L. Sais, and E. Gregoire, “Tabu search for SAT,” Proc. Natl. Conf. Artif. Intell., pp. 281–285, 1997. 231 | 232 | - [13] B. Selman, H. Levesque, and D. Mitchell, “New method for solving hard satisfiability problems,” in Proceedings Tenth National Conference on Artificial Intelligence, 1992, no. July, pp. 440–446. 233 | -------------------------------------------------------------------------------- /adaptive_memory_LS.py: -------------------------------------------------------------------------------- 1 | ''' 2 | References 3 | [1] Z. Lü and J.-K. K. Hao, “Adaptive Memory-Based Local Search for MAX-SAT,” Elsevier, Aug. 2012. doi: 10.1016/j.asoc.2012.01.013. 4 | ''' 5 | 6 | from base_solver import Base_Solver 7 | import numpy as np 8 | import random 9 | import time 10 | from itertools import chain 11 | 12 | class AMLS(Base_Solver): 13 | 14 | def __init__(self, input_cnf_file, verbose): 15 | super(AMLS, self).__init__(input_cnf_file, verbose) 16 | self.initialize_pool() 17 | self.generate() 18 | self.initialize_cost() 19 | self.best_assignment = self.assignment.copy() 20 | self.best_cost = len(self.id_unsat_clauses) 21 | self.p = 0.0 22 | self.wp = 0.0 23 | self.last_move = [-1 for _ in self.assignment] 24 | self.MAX_PERT = 15 25 | self.MAX_FLIPS = int(self.nvars*self.nvars/4) 26 | self.CHECK_FREQ = self.nvars * 10 27 | self.vf = [None for _ in self.list_clauses] 28 | self.vs = [None for _ in self.list_clauses] 29 | self.nf = [0 for _ in self.list_clauses] 30 | self.ns = [0 for _ in self.list_clauses] 31 | self.tabu_tenure = int(self.nvars/10 + 4) 32 | self.stagnation = False 33 | self.no_improvement_step = 0 34 | self.DEFINED_STEP = int(len(self.list_clauses)/6) 35 | 36 | def initialize_params(self): 37 | self.p = 0 38 | self.wp = 0 39 | self.last_move = [-1 for _ in self.assignment] 40 | self.nb_tries += 1 41 | self.nb_flips = 0 42 | self.no_improvement_step = 0 43 | 44 | 45 | def update_params(self): 46 | if self.stagnation: 47 | self.no_improvement_step += 1 48 | if self.no_improvement_step >= self.DEFINED_STEP: 49 | # Increase 50 | self.wp += float((0.05 - self.wp)/5) 51 | self.p += float((1-self.p)/5) 52 | self.no_improvement_step = 0 53 | else: 54 | # Decrease 55 | self.wp -= float(self.wp/10) 56 | self.p -= float(self.p/10) 57 | 58 | nb_moves, tb_moves = self.pick_allowed_lits(self.tabu_tenure) 59 | nb_total_moves = len(nb_moves) + len(tb_moves) 60 | self.tabu_tenure = random.randint(1,10) + int(nb_total_moves*0.25) 61 | 62 | 63 | def pick_unsat_clause(self): 64 | assert len(self.id_unsat_clauses) > 0 65 | random_index = random.choice(self.id_unsat_clauses) 66 | return self.list_clauses[random_index] 67 | 68 | def pick_allowed_lits(self, tabu_tenure): 69 | allowed_lits = [] 70 | non_allowed_lits = [] 71 | ''' 72 | GSAT strategy 73 | ''' 74 | for ind in self.id_unsat_clauses: 75 | allowed_lits += self.list_clauses[ind] 76 | allowed_lits = list(set(allowed_lits)) 77 | if tabu_tenure > 0: 78 | for lit in allowed_lits: 79 | if self.nb_flips - self.last_move[abs(lit)-1] < tabu_tenure: #tabu move 80 | allowed_lits.remove(lit) 81 | non_allowed_lits.append(lit) 82 | ''' 83 | WalkSAT strategy 84 | ''' 85 | # list_id_unsat_clauses = self.id_unsat_clauses.copy() 86 | # while len(allowed_lits) == 0 and len(list_id_unsat_clauses)>0: 87 | # random_id = random.choice(list_id_unsat_clauses) 88 | # list_id_unsat_clauses.remove(random_id) 89 | # allowed_lits = self.list_clauses[random_id] 90 | # if tabu_tenure > 0: 91 | # for lit in allowed_lits: 92 | # if self.nb_flips - self.last_move[abs(lit)-1] < tabu_tenure: #tabu move 93 | # allowed_lits.remove(lit) 94 | # non_allowed_lits.append(lit) 95 | return allowed_lits, non_allowed_lits 96 | 97 | 98 | def pick_necessary_flip(self): 99 | oldest_move = min(self.last_move) 100 | if self.nb_flips - oldest_move > self.CHECK_FREQ: 101 | return self.assignment[self.last_move.index(oldest_move)] 102 | else: 103 | return None 104 | 105 | def pick_1st_and_2nd_min(self, cost_list): 106 | assert len(cost_list) > 0 107 | x_1, x_2 = cost_list[0], cost_list[0] 108 | id_1, id_2 = 0, 0 109 | for i in range(1,len(cost_list)): 110 | if cost_list[i] <= x_1: 111 | x_1, x_2 = cost_list[i], x_1 112 | id_1, id_2 = i, id_1 113 | elif cost_list[i] < x_2: 114 | x_2 = cost_list[i] 115 | id_2 = i 116 | return id_1, id_2 117 | 118 | def penalty(self, y): 119 | list_RS, list_RF = [], [] 120 | for i in range(len(self.list_clauses)): 121 | if self.vs[i] is not None and abs(self.vs[i]) == abs(y): 122 | list_RS.append(i) 123 | if self.vf[i] is not None and abs(self.vf[i]) == abs(y): 124 | list_RF.append(i) 125 | 126 | cost_RS, cost_RF = 0, 0 127 | for cs in list_RS: 128 | cost_RS += 2**self.ns[cs] 129 | for cf in list_RF: 130 | cost_RF += 2**self.nf[cf] 131 | if len(list_RS)>0: 132 | cost_RS = float(cost_RS/(2*len(list_RS))) 133 | if len(list_RF)>0: 134 | cost_RF = float(cost_RF/(2*len(list_RF))) 135 | pen = cost_RS + cost_RF 136 | return pen 137 | 138 | def pick_neighborhood(self, tabu_tenure): 139 | ''' 140 | compute allowed literals wrt tabu list 141 | ''' 142 | allowed_lits, non_allowed_lits = self.pick_allowed_lits(tabu_tenure) 143 | if len(allowed_lits) == 0: # else take allowed_lits and ignore tabu 144 | allowed_lits, non_allowed_lits = self.pick_allowed_lits(0) 145 | ''' 146 | Compute cost of every (tabu and non tabu) moves 147 | Cost = break - make 148 | ''' 149 | assert len(allowed_lits) > 0 150 | ntb_cost, tb_cost = [], [] 151 | current_cost = len(self.id_unsat_clauses) 152 | for literal in allowed_lits: 153 | ntb_cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 154 | id_ntb_1st, id_ntb_2nd = self.pick_1st_and_2nd_min(ntb_cost) 155 | for literal in non_allowed_lits: 156 | tb_cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 157 | if len(tb_cost)>0: 158 | x_tb = non_allowed_lits[np.argmin(tb_cost)] 159 | if min(tb_cost) < min(ntb_cost) and current_cost + min(tb_cost) < self.best_cost: 160 | y = x_tb 161 | return y 162 | 163 | x_nb = allowed_lits[id_ntb_1st] 164 | x_nsb = allowed_lits[id_ntb_2nd] 165 | 166 | if min(ntb_cost) < 0: 167 | y = x_nb 168 | return y 169 | 170 | wp = random.random() 171 | if wp < self.wp: 172 | # Random walk on non tabu moves 173 | y = random.choice(allowed_lits) 174 | return y 175 | 176 | p = random.random() 177 | least_recent_move = allowed_lits[0] #largest last move 178 | for lit in allowed_lits[1:]: 179 | if self.last_move[abs(least_recent_move)-1] < self.last_move[abs(lit)-1]: 180 | least_recent_move = lit 181 | 182 | if p < self.wp and x_nb == least_recent_move: 183 | if self.penalty(x_nsb) < self.penalty(x_nb): 184 | y = x_nsb 185 | return y 186 | 187 | y = x_nb 188 | return y 189 | 190 | def flip(self, literal): 191 | self.nb_flips += 1 192 | # Flip variable in assignment 193 | ind = 0 194 | if literal in self.assignment: 195 | ind = self.assignment.index(literal) 196 | elif -literal in self.assignment: 197 | ind = self.assignment.index(-literal) 198 | old_literal = self.assignment[ind] 199 | self.assignment[ind] *= -1 200 | # Update cost 201 | # Clause contains literal => cost -- 202 | if old_literal in self.pool.keys(): 203 | for i in self.pool[old_literal]: 204 | self.costs[i] -= 1 205 | if self.costs[i] == 0: # if SAT -> UNSAT: add to list of unsat clauses 206 | self.id_unsat_clauses.append(i) 207 | if self.vf[i] is not None and self.vf[i] == abs(literal): 208 | self.nf[i] += 1 209 | else: 210 | self.vf[i] = abs(literal) 211 | self.nf[i] = 1 212 | # Clause contains -literal => cost ++ 213 | if -old_literal in self.pool.keys(): 214 | for j in self.pool[-old_literal]: 215 | if self.costs[j] == 0: # if UNSAT -> SAT: remove from list of unsat clauses 216 | self.id_unsat_clauses.remove(j) 217 | if self.vs[j] is not None and self.vs[j] == abs(literal): 218 | self.ns[j] += 1 219 | else: 220 | self.vs[j] = abs(literal) 221 | self.ns[j] = 1 222 | self.costs[j] += 1 223 | 224 | def perturbate(self, tabu_tenure): 225 | nb_pert = 0 226 | while nb_pert < self.MAX_PERT and not self.check(): 227 | ''' 228 | compute allowed literals wrt tabu list 229 | ''' 230 | all_allowed_lits, non_allowed_lits = self.pick_allowed_lits(tabu_tenure) 231 | if len(all_allowed_lits) == 0: # else take all_allowed_lits and ignore tabu 232 | all_allowed_lits, non_allowed_lits = self.pick_allowed_lits(0) 233 | ''' 234 | Compute cost of every (tabu and non tabu) moves 235 | Cost = break - make 236 | ''' 237 | ntb_cost, tb_cost = [], [] 238 | current_cost = len(self.id_unsat_clauses) 239 | for literal in all_allowed_lits: 240 | ntb_cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 241 | x_ntb = all_allowed_lits[np.argmin(ntb_cost)] 242 | for literal in non_allowed_lits: 243 | tb_cost.append(self.evaluate_breakcount(literal, bs=1, ms=1)) 244 | if len(tb_cost) > 0: 245 | x_tb = non_allowed_lits[np.argmin(tb_cost)] 246 | if min(tb_cost) < min(ntb_cost) and current_cost + min(tb_cost) < self.best_cost: #EXCEPTION 247 | x = x_tb 248 | else: 249 | x = x_ntb 250 | else: 251 | x = x_ntb 252 | 253 | self.flip(x) 254 | self.last_move[abs(x)-1] = self.nb_flips 255 | if len(self.id_unsat_clauses) < self.best_cost: 256 | self.best_cost = len(self.id_unsat_clauses) 257 | ''' 258 | Every 10n iterations, if a variable is not flipped within 10n iterations 259 | => force X to be flipped ! 260 | ''' 261 | if self.nb_flips % self.CHECK_FREQ == 0: 262 | x = self.pick_necessary_flip() 263 | if x is not None: 264 | self.flip(x) 265 | self.last_move[abs(x)-1] = self.nb_flips 266 | if len(self.id_unsat_clauses) < self.best_cost: 267 | self.best_cost = len(self.id_unsat_clauses) 268 | ''' 269 | TODO: Every n iterations => change randomly tabu tenure 270 | Note: tabu tenure for perturbation phase should be larger than the one used for LS 271 | ''' 272 | # if self.nb_flips % self.nvars == 0: 273 | # self.tabu_tenure = random.randint(self.tabu_tenure_MIN, self.tabu_tenure_MAX) 274 | nb_pert += 1 275 | return self.assignment 276 | 277 | def solve(self): 278 | initial = time.time() 279 | while self.nb_tries < self.MAX_TRIES and not self.is_sat: 280 | ''' 281 | Search Phase 282 | ''' 283 | self.initialize_params() 284 | while self.nb_flips < self.MAX_FLIPS and not self.check(): 285 | ''' 286 | Select move 287 | ''' 288 | x = self.pick_neighborhood(self.tabu_tenure) 289 | self.flip(x) 290 | ''' 291 | Update best cost and assignment 292 | ''' 293 | if len(self.id_unsat_clauses) < self.best_cost: 294 | self.best_cost = len(self.id_unsat_clauses) 295 | self.best_assignment = self.assignment.copy() 296 | self.stagnation = False 297 | else: 298 | self.stagnation = True 299 | ''' 300 | Add this move to the tabu list 301 | Update p, wp, tabu tenure 302 | ''' 303 | self.last_move[abs(x)-1] = self.nb_flips 304 | self.update_params() 305 | ''' 306 | Perturbation Phase 307 | ''' 308 | self.assignment = self.perturbate(int(self.nvars/2)) 309 | if self.check(): 310 | self.is_sat = True 311 | 312 | end = time.time() 313 | print('Nb flips: {0} '.format(self.nb_flips)) 314 | print('Nb tries: {0} '.format(self.nb_tries)) 315 | print('CPU time: {0:10.4f} s '.format(end-initial)) 316 | if self.is_sat: 317 | print('SAT') 318 | return self.assignment 319 | else: 320 | print('UNKNOWN') 321 | return None 322 | 323 | 324 | --------------------------------------------------------------------------------