|
| 1 | +from threading import Timer |
| 2 | +from time import perf_counter |
| 3 | + |
| 4 | +from pysat.formula import CNF |
| 5 | +from pysat.solvers import Solver |
| 6 | + |
| 7 | + |
| 8 | +class NQueensSATModeler: |
| 9 | + """ |
| 10 | + Modeler class for the n queens problem using SAT. |
| 11 | + """ |
| 12 | + |
| 13 | + def __init__(self, n): |
| 14 | + self.n = n |
| 15 | + self.cnf: CNF | None = None |
| 16 | + self._cnf_breaks_symmetries = False |
| 17 | + |
| 18 | + def create_formula(self, break_symmetries=False) -> CNF: |
| 19 | + """ |
| 20 | + Create a CNF formula representing the N-queens problem. |
| 21 | + If break_symmetries is True, this method may add additional |
| 22 | + constraints that break a mirror symmetry of the problem, |
| 23 | + which may help some SAT solvers find solutions faster. |
| 24 | + The only hard requirement is that, if break_symmetries is False, |
| 25 | + the returned formula must be satisfied by all valid N-queens solutions. |
| 26 | + """ |
| 27 | + if self.cnf is not None and self._cnf_breaks_symmetries == break_symmetries: |
| 28 | + return self.cnf |
| 29 | + self.cnf = CNF() |
| 30 | + self._cnf_breaks_symmetries = break_symmetries |
| 31 | + # TODO: add clauses to self.cnf (via self.cnf.append or self.cnf.extend); |
| 32 | + # e.g., self.cnf.append([1, -2, 3, 4]) for (x1 OR NOT x2 OR x3 OR x4) |
| 33 | + raise NotImplementedError() |
| 34 | + return self.cnf |
| 35 | + |
| 36 | + def decode_solution(self, model: list[int]) -> list[list[bool]]: |
| 37 | + """ |
| 38 | + Decodes a model (list of literals like [1, -2, 3, -4, ...]) |
| 39 | + as returned by a PySAT SAT solver into a 2D list of booleans. |
| 40 | + """ |
| 41 | + # TODO |
| 42 | + raise NotImplementedError() |
| 43 | + |
| 44 | + def encode_assignment(self, assignment: list[list[bool]]) -> list[int]: |
| 45 | + """ |
| 46 | + Encodes a 2D list of booleans representing an N-queens solution |
| 47 | + into a list of literals (like [1, -2, 3, -4, ...]) that can be used |
| 48 | + as input to a PySAT SAT solver or be used by debug/test methods to |
| 49 | + check that the formula is satisfied by what should be a satisfying assignment. |
| 50 | + As long as the formula created by create_formula was created with |
| 51 | + break_symmetries=False, the returned list of literals should satisfy |
| 52 | + the formula if and only if the input assignment is a valid N-queens solution. |
| 53 | + """ |
| 54 | + # TODO |
| 55 | + raise NotImplementedError() |
| 56 | + |
| 57 | + |
| 58 | +def solve_with_sat( |
| 59 | + n, |
| 60 | + break_symmetries=False, |
| 61 | + solver_name="Cadical300", |
| 62 | + time_limit: float | None = None, |
| 63 | + times: dict | None = None, |
| 64 | +) -> list[list[bool]] | bool | None: |
| 65 | + """ |
| 66 | + Run the named SAT solver on the given n (potentially with symmetry breaking |
| 67 | + constraints, if implemented) and with a time limit. |
| 68 | + This method should not need to be changed; all necessary changes should be |
| 69 | + local to the NQueensSATModeler class. |
| 70 | + """ |
| 71 | + before = perf_counter() |
| 72 | + modeler = NQueensSATModeler(n) |
| 73 | + formula = modeler.create_formula(break_symmetries=break_symmetries) |
| 74 | + solver = Solver(name=solver_name) |
| 75 | + timer = None |
| 76 | + |
| 77 | + def interrupt(): |
| 78 | + solver.interrupt() |
| 79 | + |
| 80 | + try: |
| 81 | + solver.append_formula(formula.clauses, no_return=True) |
| 82 | + build = perf_counter() |
| 83 | + if times is not None: |
| 84 | + times["build_time"] = build - before |
| 85 | + if time_limit is not None: |
| 86 | + timer = Timer(time_limit, interrupt, [solver]) |
| 87 | + timer.start() |
| 88 | + r = solver.solve_limited(expect_interrupt=True) |
| 89 | + else: |
| 90 | + r = solver.solve() |
| 91 | + after = perf_counter() |
| 92 | + if times is not None: |
| 93 | + times["solve_time"] = after - build |
| 94 | + if r is None: |
| 95 | + return None |
| 96 | + if r is False: |
| 97 | + return False |
| 98 | + model: list[int] = solver.get_model() # type: ignore |
| 99 | + return modeler.decode_solution(model) |
| 100 | + finally: |
| 101 | + if timer is not None: |
| 102 | + timer.cancel() |
| 103 | + solver.delete() |
0 commit comments