def test_minimal_unsatisfiable_subset(): assert raises(ValueError, lambda: minimal_unsatisfiable_subset([[1]])) clauses = [[-10], [1], [5], [2, 3], [3, 4], [5, 2], [-7], [2], [3], [-2, -3, 5], [7, 8, 9, 10], [-8], [-9]] res = minimal_unsatisfiable_subset(clauses) assert sorted(res) == [[-10], [-9], [-8], [-7], [7, 8, 9, 10]] assert not sat(res) clauses = [[1, 3], [2, 3], [-1], [4], [3], [-3]] for perm in permutations(clauses): res = minimal_unsatisfiable_subset(clauses) assert sorted(res) == [[-3], [3]] assert not sat(res) clauses = [[1], [-1], [2], [-2], [3, 4], [4]] for perm in permutations(clauses): res = minimal_unsatisfiable_subset(perm) assert sorted(res) in [[[-1], [1]], [[-2], [2]]] assert not sat(res)
def minimal_unsatisfiable_subset(self, clauses, v, w): while True: for i in combinations(clauses, len(clauses) - 1): if not sat(list(i)): dotlog.debug('Finding minimal unsatisfiable subset') clauses = i break else: break import pprint return "The following set of clauses is unsatisfiable\n%s" % \ pprint.pformat([[w[j] if j > 0 else 'not ' + w[-j] for j in k] for k in i])
def minimal_unsatisfiable_subset(self, clauses, v, w): while True: for i in combinations(clauses, len(clauses) - 1): if not sat(list(i)): sys.stdout.write('.');sys.stdout.flush() clauses = i break else: break import pprint print() print("The following set of clauses is unsatisfiable") pprint.pprint([[w[j] if j > 0 else 'not ' + w[-j] for j in k] for k in i])
def minimal_unsatisfiable_subset(self, clauses, v, w): while True: for i in combinations(clauses, len(clauses) - 1): if not sat(list(i)): sys.stdout.write('.') sys.stdout.flush() clauses = i break else: break import pprint print() print("The following set of clauses is unsatisfiable") pprint.pprint([[w[j] if j > 0 else 'not ' + w[-j] for j in k] for k in i])
def solve(self, specs, installed=[], update_deps=True, returnall=False, guess=True, minimal_hint=False): try: stdoutlog.info("Solving package specifications: ") res = self.explicit(specs) if res is not None: return res # If update_deps=True, set the target package in MatchSpec so that # the solver can minimize the version change. If update_deps=False, # fix the version and build so that no change is possible. len0 = len(specs) specs = list(map(MatchSpec, specs)) snames = {s.name for s in specs} for pkg in installed: name, version, build = self.package_triple(pkg) if pkg not in self.index: self.index[pkg] = { 'name':name, 'version':version, 'build':build, 'build_number':0 } self.groups.setdefault(name,[]).append(pkg) if name in snames: continue if update_deps: spec = MatchSpec(name, target=pkg) else: spec = MatchSpec('%s %s %s'%(name,version,build)) specs.append(spec) snames.add(spec) dotlog.debug("Solving for %s" % specs) try: dists, specs = self.get_dists(specs) except NoPackagesFound: raise # Clear out our caches to reduce memory usage before the solve self.find_matches_.clear() self.ms_depends_.clear() # Check if satisfiable dotlog.debug('Checking satisfiability') groups = build_groups(dists) m, v, w = self.build_vw(groups) clauses = list(self.gen_clauses(v, groups, specs)) solution = sat(clauses) if not solution: if guess: if minimal_hint: stderrlog.info('\nError: Unsatisfiable package ' 'specifications.\nGenerating minimal hint: \n') sys.exit(self.minimal_unsatisfiable_subset(clauses, v, w)) else: stderrlog.info('\nError: Unsatisfiable package ' 'specifications.\nGenerating hint: \n') sys.exit(self.guess_bad_solve(specs)) raise RuntimeError("Unsatisfiable package specifications") eq_version = self.generate_version_eq(v, groups, specs[:len0], majoronly=True) clauses, solution, obj1 = optimize(eq_version, clauses, solution) dotlog.debug('Requested version metric: %d'%obj1) eq_features, n0 = self.generate_feature_eq(v, groups, specs) clauses, solution, obj = optimize(eq_features, clauses, solution) dotlog.debug('Feature count metric: %d'%(obj+n0)) eq_version2 = self.generate_version_eq(v, groups, specs, majoronly=False) clauses, solution, obj2 = optimize(eq_version2, clauses, solution) dotlog.debug('Total version metric: %d'%obj2) eq_version3 = self.generate_package_count(v, groups, specs) clauses, solution, obj3 = optimize(eq_version3, clauses, solution) dotlog.debug('Weak dependency metric: %d'%obj3) dotlog.debug('Final metrics: (%d,%d,%d,%d)'%( (n0+evaluate_eq(eq_features,solution), evaluate_eq(eq_version,solution), evaluate_eq(eq_version2,solution), evaluate_eq(eq_version3,solution)))) solution = [s for s in solution if 0 < s <= m] dotlog.debug('Looking for alternate solutions') solutions = [solution] nsol = 1 while True: nclause = tuple(-q for q in solution if 0 < q <= m) clauses.append(nclause) solution = sat(clauses) if solution is None: break solution = [s for s in solution if 0 < s <= m] nsol += 1 if nsol > 10: dotlog.debug('Too many solutions; terminating') break solutions.append(solution) psolutions = [set(w[lit] for lit in sol if 0 < lit <= m and '@' not in w[lit]) for sol in solutions] if nsol > 1: stdoutlog.info( '\nWarning: %s possible package resolutions (only showing differing packages):\n' % ('>10' if nsol > 10 else nsol)) common = set.intersection(*psolutions) for sol in psolutions: stdoutlog.info('\t%s,\n' % sorted(sol - common)) if obj1 > 0 or obj2 > 0 or (obj3 > 0 and any(i>1 for i,_ in eq_version3)): log.debug("Older versions in the solution(s):") for sol in solutions: v = ([(i, w[j]) for i, j in eq_version if j in sol] + [(i, w[j]) for i, j in eq_version2 if j in sol] + [(i, w[j]) for i, j in eq_version3 if i>1 and j in sol]) log.debug(v) stdoutlog.info('\n') return list(map(sorted, psolutions)) if returnall else sorted(psolutions[0]) except: stdoutlog.info('\n') raise
def mysat(specs): dists = self.get_dists(specs) groups = build_groups(dists) m, v, w = self.build_vw(groups) clauses = set(self.gen_clauses(v, groups, specs)) return sat(clauses)
def solve2(self, specs, features, guess=True, alg='BDD', returnall=False, minimal_hint=False, unsat_only=False): log.debug("Solving for %s" % str(specs)) log.debug("Using alg %s" % alg) # First try doing it the "old way", i.e., just look at the most recent # version of each package from the specs. This doesn't handle the more # complicated cases that the pseudo-boolean solver does, but it's also # much faster when it does work. try: dists = self.get_dists(specs, max_only=True) except NoPackagesFound: # Handle packages that are not included because some dependencies # couldn't be found. pass else: v = {} # map fn to variable number w = {} # map variable number to fn i = -1 # in case the loop doesn't run for i, fn in enumerate(sorted(dists)): v[fn] = i + 1 w[i + 1] = fn m = i + 1 dotlog.debug("Solving using max dists only") clauses = set(self.gen_clauses(v, dists, specs, features)) solutions = min_sat(clauses) if len(solutions) == 1: ret = [w[lit] for lit in solutions.pop(0) if 0 < lit <= m] if returnall: return [ret] return ret dists = self.get_dists(specs) v = {} # map fn to variable number w = {} # map variable number to fn i = -1 # in case the loop doesn't run for i, fn in enumerate(sorted(dists)): v[fn] = i + 1 w[i + 1] = fn m = i + 1 clauses = set(self.gen_clauses(v, dists, specs, features)) if not clauses: if returnall: return [[]] return [] eq, max_rhs = self.generate_version_eq(v, dists) # Second common case, check if it's unsatisfiable dotlog.debug("Checking for unsatisfiability") solution = sat(clauses) if not solution: if guess: stderrlog.info('\nError: Unsatisfiable package ' 'specifications.\nGenerating hint: ') if minimal_hint: sys.exit(self.minimal_unsatisfiable_subset(clauses, v, w)) else: sys.exit(self.guess_bad_solve(specs, features)) raise RuntimeError("Unsatisfiable package specifications") if unsat_only: return True def version_constraints(lo, hi): return set(generate_constraints(eq, m, [lo, hi], alg=alg)) log.debug("Bisecting the version constraint") evaluate_func = partial(evaluate_eq, eq) constraints = bisect_constraints(0, max_rhs, clauses, version_constraints, evaluate_func=evaluate_func) # Only relevant for build_BDD if constraints and false in constraints: # XXX: This should *never* happen. build_BDD only returns false # when the linear constraint is unsatisfiable, but any linear # constraint can equal 0, by setting all the variables to 0. solution = [] else: if constraints and true in constraints: constraints = set([]) dotlog.debug("Finding the minimal solution") solutions = min_sat(clauses | constraints, N=m + 1) assert solutions, (specs, features) if len(solutions) > 1: stdoutlog.info('Warning: %s possible package resolutions:' % len(solutions)) for sol in solutions: stdoutlog.info('\t' + str([w[lit] for lit in sol if 0 < lit <= m])) if returnall: return [[w[lit] for lit in sol if 0 < lit <= m] for sol in solutions] return [w[lit] for lit in solutions.pop(0) if 0 < lit <= m]
def solve2(self, specs, features, installed=(), guess=True, alg='BDD', returnall=False, minimal_hint=False, unsat_only=False, update_deps=True, try_max_only=None): log.debug("Solving for %s" % str(specs)) log.debug("Features: %s" % str(features)) log.debug("Installed: %s" % str(installed)) # This won't packages that aren't in the index, but there isn't much # we can do with such packages here anyway. installed_dists = { pkg: Package(pkg, self.index[pkg]) for pkg in installed if pkg in self.index } if try_max_only is None: if unsat_only or update_deps: try_max_only = False else: try_max_only = True # XXX: Should try_max_only use the filtered list? if try_max_only: try: dists = self.get_dists(specs, max_only=True, filtered=False) except NoPackagesFound: # Handle packages that are not included because some dependencies # couldn't be found. pass else: v = {} # map fn to variable number w = {} # map variable number to fn i = -1 # in case the loop doesn't run for i, fn in enumerate(sorted(dists)): v[fn] = i + 1 w[i + 1] = fn m = i + 1 dotlog.debug("Solving using max dists only") clauses = set(self.gen_clauses(v, dists, specs, features)) try: solutions = min_sat(clauses, alg='iterate', raise_on_max_n=True) except MaximumIterationsError: pass else: if len(solutions) == 1: ret = [ w[lit] for lit in solutions.pop(0) if 0 < lit <= m ] if returnall: return [ret] return ret dists = self.get_dists(specs, filtered=True) v = {} # map fn to variable number w = {} # map variable number to fn i = -1 # in case the loop doesn't run for i, fn in enumerate(sorted(dists)): v[fn] = i + 1 w[i + 1] = fn m = i + 1 clauses = set(self.gen_clauses(v, dists, specs, features)) if not clauses: if returnall: return [[]] return [] eq, max_rhs = self.generate_version_eq(v, dists, installed_dists, specs, update_deps=update_deps) # Second common case, check if it's unsatisfiable dotlog.debug("Checking for unsatisfiability") solution = sat(clauses) if not solution: if guess: if minimal_hint: stderrlog.info( '\nError: Unsatisfiable package ' 'specifications.\nGenerating minimal hint: \n') sys.exit(self.minimal_unsatisfiable_subset(clauses, v, w)) else: stderrlog.info('\nError: Unsatisfiable package ' 'specifications.\nGenerating hint: \n') sys.exit(self.guess_bad_solve(specs, features)) raise RuntimeError("Unsatisfiable package specifications") if unsat_only: return True log.debug("Using alg %s" % alg) def version_constraints(lo, hi): return set(generate_constraints(eq, m, [lo, hi], alg=alg)) log.debug("Bisecting the version constraint") evaluate_func = partial(evaluate_eq, eq) constraints = bisect_constraints(0, max_rhs, clauses, version_constraints, evaluate_func=evaluate_func) # Only relevant for build_BDD if constraints and false in constraints: # XXX: This should *never* happen. build_BDD only returns false # when the linear constraint is unsatisfiable, but any linear # constraint can equal 0, by setting all the variables to 0. solution = [] else: if constraints and true in constraints: constraints = set([]) dotlog.debug("Finding the minimal solution") try: solutions = min_sat(clauses | constraints, N=m + 1, alg='iterate', raise_on_max_n=True) except MaximumIterationsError: solutions = min_sat(clauses | constraints, N=m + 1, alg='sorter') assert solutions, (specs, features) if len(solutions) > 1: stdoutlog.info( '\nWarning: %s possible package resolutions (only showing differing packages):\n' % len(solutions)) pretty_solutions = [{w[lit] for lit in sol if 0 < lit <= m} for sol in solutions] common = set.intersection(*pretty_solutions) for sol in pretty_solutions: stdoutlog.info('\t%s,\n' % sorted(sol - common)) log.debug("Older versions in the solution(s):") for sol in solutions: log.debug([(i, w[j]) for i, j in eq if j in sol]) if returnall: return [[w[lit] for lit in sol if 0 < lit <= m] for sol in solutions] return [w[lit] for lit in solutions.pop(0) if 0 < lit <= m]
def solve2(self, specs, features, guess=True, alg='sorter', returnall=False): log.debug("Solving for %s" % str(specs)) # First try doing it the "old way", i.e., just look at the most recent # version of each package from the specs. This doesn't handle the more # complicated cases that the pseudo-boolean solver does, but it's also # much faster when it does work. try: dists = self.get_dists(specs, max_only=True) except NoPackagesFound: # Handle packages that are not included because some dependencies # couldn't be found. pass else: v = {} # map fn to variable number w = {} # map variable number to fn i = -1 # in case the loop doesn't run for i, fn in enumerate(sorted(dists)): v[fn] = i + 1 w[i + 1] = fn m = i + 1 dotlog.debug("Solving using max dists only") clauses = self.gen_clauses(v, dists, specs, features) solutions = min_sat(clauses) if len(solutions) == 1: ret = [w[lit] for lit in solutions.pop(0) if 0 < lit] if returnall: return [ret] return ret dists = self.get_dists(specs) v = {} # map fn to variable number w = {} # map variable number to fn i = -1 # in case the loop doesn't run for i, fn in enumerate(sorted(dists)): v[fn] = i + 1 w[i + 1] = fn m = i + 1 clauses = list(self.gen_clauses(v, dists, specs, features)) if not clauses: if returnall: return [[]] return [] eq, max_rhs = self.generate_version_eq(v, dists) # Check the common case first dotlog.debug("Building the constraint with rhs: [0, 0]") constraints = list(generate_constraints(eq, m, [0, 0], alg=alg)) # Only relevant for build_BDD if constraints and constraints[0] == [false]: # XXX: This should *never* happen. build_BDD only returns false # when the linear constraint is unsatisfiable, but any linear # constraint can equal 0, by setting all the variables to 0. solution = [] else: if constraints and constraints[0] == [true]: constraints = [] dotlog.debug("Checking for solutions with rhs: [0, 0]") solution = sat(clauses + constraints) if not solution: # Second common case, check if it's unsatisfiable dotlog.debug("Checking for unsatisfiability") solution = sat(clauses) if not solution: if guess: stderrlog.info('\nError: Unsatisfiable package ' 'specifications.\nGenerating hint: ') sys.exit(self.guess_bad_solve(specs, features)) raise RuntimeError("Unsatisfiable package specifications") def version_constraints(lo, hi): return list(generate_constraints(eq, m, [lo, hi], alg=alg)) log.debug("Bisecting the version constraint") constraints = bisect_constraints(0, max_rhs, clauses, version_constraints) dotlog.debug("Finding the minimal solution") solutions = min_sat(clauses + constraints, N=m+1) assert solutions, (specs, features) if len(solutions) > 1: print('Warning:', len(solutions), "possible package resolutions:") for sol in solutions: print('\t', [w[lit] for lit in sol if 0 < lit <= m]) if returnall: return [[w[lit] for lit in sol if 0 < lit <= m] for sol in solutions] return [w[lit] for lit in solutions.pop(0) if 0 < lit <= m]
def test_sat(): assert sat([[1]]) == [1] assert sat([[1], [-1]]) == [] assert sat([]) == []
def solve2(self, specs, features, guess=True, alg='BDD', returnall=False, minimal_hint=False, unsat_only=False): log.debug("Solving for %s" % str(specs)) # First try doing it the "old way", i.e., just look at the most recent # version of each package from the specs. This doesn't handle the more # complicated cases that the pseudo-boolean solver does, but it's also # much faster when it does work. try: dists = self.get_dists(specs, max_only=True) except NoPackagesFound: # Handle packages that are not included because some dependencies # couldn't be found. pass else: v = {} # map fn to variable number w = {} # map variable number to fn i = -1 # in case the loop doesn't run for i, fn in enumerate(sorted(dists)): v[fn] = i + 1 w[i + 1] = fn m = i + 1 dotlog.debug("Solving using max dists only") clauses = set(self.gen_clauses(v, dists, specs, features)) try: solutions = min_sat(clauses, alg='iterate', raise_on_max_n=True) except MaximumIterationsError: pass else: if len(solutions) == 1: ret = [w[lit] for lit in solutions.pop(0) if 0 < lit <= m] if returnall: return [ret] return ret dists = self.get_dists(specs) v = {} # map fn to variable number w = {} # map variable number to fn i = -1 # in case the loop doesn't run for i, fn in enumerate(sorted(dists)): v[fn] = i + 1 w[i + 1] = fn m = i + 1 clauses = set(self.gen_clauses(v, dists, specs, features)) if not clauses: if returnall: return [[]] return [] eq, max_rhs = self.generate_version_eq(v, dists) # Second common case, check if it's unsatisfiable dotlog.debug("Checking for unsatisfiability") solution = sat(clauses) if not solution: if guess: if minimal_hint: stderrlog.info('\nError: Unsatisfiable package ' 'specifications.\nGenerating minimal hint: \n') sys.exit(self.minimal_unsatisfiable_subset(clauses, v, w)) else: stderrlog.info('\nError: Unsatisfiable package ' 'specifications.\nGenerating hint: \n') sys.exit(self.guess_bad_solve(specs, features)) raise RuntimeError("Unsatisfiable package specifications") if unsat_only: return True log.debug("Using alg %s" % alg) def version_constraints(lo, hi): return set(generate_constraints(eq, m, [lo, hi], alg=alg)) log.debug("Bisecting the version constraint") evaluate_func = partial(evaluate_eq, eq) constraints = bisect_constraints(0, max_rhs, clauses, version_constraints, evaluate_func=evaluate_func) # Only relevant for build_BDD if constraints and false in constraints: # XXX: This should *never* happen. build_BDD only returns false # when the linear constraint is unsatisfiable, but any linear # constraint can equal 0, by setting all the variables to 0. solution = [] else: if constraints and true in constraints: constraints = set([]) dotlog.debug("Finding the minimal solution") try: solutions = min_sat(clauses | constraints, N=m + 1, alg='iterate', raise_on_max_n=True) except MaximumIterationsError: solutions = min_sat(clauses | constraints, N=m + 1, alg='sorter') assert solutions, (specs, features) if len(solutions) > 1: stdoutlog.info('\nWarning: %s possible package resolutions (only showing differing packages):\n' % len(solutions)) pretty_solutions = [{w[lit] for lit in sol if 0 < lit <= m} for sol in solutions] common = set.intersection(*pretty_solutions) for sol in pretty_solutions: stdoutlog.info('\t%s,\n' % sorted(sol - common)) if returnall: return [[w[lit] for lit in sol if 0 < lit <= m] for sol in solutions] return [w[lit] for lit in solutions.pop(0) if 0 < lit <= m]
def solve2(self, specs, features, installed=(), guess=True, alg='BDD', returnall=False, minimal_hint=False, unsat_only=False, update_deps=True, try_max_only=None): log.debug("Solving for %s" % str(specs)) log.debug("Features: %s" % str(features)) log.debug("Installed: %s" % str(installed)) # This won't packages that aren't in the index, but there isn't much # we can do with such packages here anyway. installed_dists = {pkg: Package(pkg, self.index[pkg]) for pkg in installed if pkg in self.index} if try_max_only is None: if unsat_only or update_deps: try_max_only = False else: try_max_only = True if try_max_only: try: dists = self.get_dists(specs, max_only=True) except NoPackagesFound: # Handle packages that are not included because some dependencies # couldn't be found. pass else: v = {} # map fn to variable number w = {} # map variable number to fn i = -1 # in case the loop doesn't run for i, fn in enumerate(sorted(dists)): v[fn] = i + 1 w[i + 1] = fn m = i + 1 dotlog.debug("Solving using max dists only") clauses = set(self.gen_clauses(v, dists, specs, features)) try: solutions = min_sat(clauses, alg='iterate', raise_on_max_n=True) except MaximumIterationsError: pass else: if len(solutions) == 1: ret = [w[lit] for lit in solutions.pop(0) if 0 < lit <= m] if returnall: return [ret] return ret dists = self.get_dists(specs) v = {} # map fn to variable number w = {} # map variable number to fn i = -1 # in case the loop doesn't run for i, fn in enumerate(sorted(dists)): v[fn] = i + 1 w[i + 1] = fn m = i + 1 clauses = set(self.gen_clauses(v, dists, specs, features)) if not clauses: if returnall: return [[]] return [] eq, max_rhs = self.generate_version_eq(v, dists, installed_dists, specs, update_deps=update_deps) # Second common case, check if it's unsatisfiable dotlog.debug("Checking for unsatisfiability") solution = sat(clauses) if not solution: if guess: if minimal_hint: stderrlog.info('\nError: Unsatisfiable package ' 'specifications.\nGenerating minimal hint: \n') sys.exit(self.minimal_unsatisfiable_subset(clauses, v, w)) else: stderrlog.info('\nError: Unsatisfiable package ' 'specifications.\nGenerating hint: \n') sys.exit(self.guess_bad_solve(specs, features)) raise RuntimeError("Unsatisfiable package specifications") if unsat_only: return True log.debug("Using alg %s" % alg) def version_constraints(lo, hi): return set(generate_constraints(eq, m, [lo, hi], alg=alg)) log.debug("Bisecting the version constraint") evaluate_func = partial(evaluate_eq, eq) constraints = bisect_constraints(0, max_rhs, clauses, version_constraints, evaluate_func=evaluate_func) # Only relevant for build_BDD if constraints and false in constraints: # XXX: This should *never* happen. build_BDD only returns false # when the linear constraint is unsatisfiable, but any linear # constraint can equal 0, by setting all the variables to 0. solution = [] else: if constraints and true in constraints: constraints = set([]) dotlog.debug("Finding the minimal solution") try: solutions = min_sat(clauses | constraints, N=m + 1, alg='iterate', raise_on_max_n=True) except MaximumIterationsError: solutions = min_sat(clauses | constraints, N=m + 1, alg='sorter') assert solutions, (specs, features) if len(solutions) > 1: stdoutlog.info('\nWarning: %s possible package resolutions (only showing differing packages):\n' % len(solutions)) pretty_solutions = [{w[lit] for lit in sol if 0 < lit <= m} for sol in solutions] common = set.intersection(*pretty_solutions) for sol in pretty_solutions: stdoutlog.info('\t%s,\n' % sorted(sol - common)) log.debug("Older versions in the solution(s):") for sol in solutions: log.debug([(i, w[j]) for i, j in eq if j in sol]) if returnall: return [[w[lit] for lit in sol if 0 < lit <= m] for sol in solutions] return [w[lit] for lit in solutions.pop(0) if 0 < lit <= m]