dpll2.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. """Implementation of DPLL algorithm
  2. Features:
  3. - Clause learning
  4. - Watch literal scheme
  5. - VSIDS heuristic
  6. References:
  7. - https://en.wikipedia.org/wiki/DPLL_algorithm
  8. """
  9. from collections import defaultdict
  10. from heapq import heappush, heappop
  11. from sympy.core.sorting import ordered
  12. from sympy.assumptions.cnf import EncodedCNF
  13. def dpll_satisfiable(expr, all_models=False):
  14. """
  15. Check satisfiability of a propositional sentence.
  16. It returns a model rather than True when it succeeds.
  17. Returns a generator of all models if all_models is True.
  18. Examples
  19. ========
  20. >>> from sympy.abc import A, B
  21. >>> from sympy.logic.algorithms.dpll2 import dpll_satisfiable
  22. >>> dpll_satisfiable(A & ~B)
  23. {A: True, B: False}
  24. >>> dpll_satisfiable(A & ~A)
  25. False
  26. """
  27. if not isinstance(expr, EncodedCNF):
  28. exprs = EncodedCNF()
  29. exprs.add_prop(expr)
  30. expr = exprs
  31. # Return UNSAT when False (encoded as 0) is present in the CNF
  32. if {0} in expr.data:
  33. if all_models:
  34. return (f for f in [False])
  35. return False
  36. solver = SATSolver(expr.data, expr.variables, set(), expr.symbols)
  37. models = solver._find_model()
  38. if all_models:
  39. return _all_models(models)
  40. try:
  41. return next(models)
  42. except StopIteration:
  43. return False
  44. # Uncomment to confirm the solution is valid (hitting set for the clauses)
  45. #else:
  46. #for cls in clauses_int_repr:
  47. #assert solver.var_settings.intersection(cls)
  48. def _all_models(models):
  49. satisfiable = False
  50. try:
  51. while True:
  52. yield next(models)
  53. satisfiable = True
  54. except StopIteration:
  55. if not satisfiable:
  56. yield False
  57. class SATSolver:
  58. """
  59. Class for representing a SAT solver capable of
  60. finding a model to a boolean theory in conjunctive
  61. normal form.
  62. """
  63. def __init__(self, clauses, variables, var_settings, symbols=None,
  64. heuristic='vsids', clause_learning='none', INTERVAL=500):
  65. self.var_settings = var_settings
  66. self.heuristic = heuristic
  67. self.is_unsatisfied = False
  68. self._unit_prop_queue = []
  69. self.update_functions = []
  70. self.INTERVAL = INTERVAL
  71. if symbols is None:
  72. self.symbols = list(ordered(variables))
  73. else:
  74. self.symbols = symbols
  75. self._initialize_variables(variables)
  76. self._initialize_clauses(clauses)
  77. if 'vsids' == heuristic:
  78. self._vsids_init()
  79. self.heur_calculate = self._vsids_calculate
  80. self.heur_lit_assigned = self._vsids_lit_assigned
  81. self.heur_lit_unset = self._vsids_lit_unset
  82. self.heur_clause_added = self._vsids_clause_added
  83. # Note: Uncomment this if/when clause learning is enabled
  84. #self.update_functions.append(self._vsids_decay)
  85. else:
  86. raise NotImplementedError
  87. if 'simple' == clause_learning:
  88. self.add_learned_clause = self._simple_add_learned_clause
  89. self.compute_conflict = self.simple_compute_conflict
  90. self.update_functions.append(self.simple_clean_clauses)
  91. elif 'none' == clause_learning:
  92. self.add_learned_clause = lambda x: None
  93. self.compute_conflict = lambda: None
  94. else:
  95. raise NotImplementedError
  96. # Create the base level
  97. self.levels = [Level(0)]
  98. self._current_level.varsettings = var_settings
  99. # Keep stats
  100. self.num_decisions = 0
  101. self.num_learned_clauses = 0
  102. self.original_num_clauses = len(self.clauses)
  103. def _initialize_variables(self, variables):
  104. """Set up the variable data structures needed."""
  105. self.sentinels = defaultdict(set)
  106. self.occurrence_count = defaultdict(int)
  107. self.variable_set = [False] * (len(variables) + 1)
  108. def _initialize_clauses(self, clauses):
  109. """Set up the clause data structures needed.
  110. For each clause, the following changes are made:
  111. - Unit clauses are queued for propagation right away.
  112. - Non-unit clauses have their first and last literals set as sentinels.
  113. - The number of clauses a literal appears in is computed.
  114. """
  115. self.clauses = [list(clause) for clause in clauses]
  116. for i, clause in enumerate(self.clauses):
  117. # Handle the unit clauses
  118. if 1 == len(clause):
  119. self._unit_prop_queue.append(clause[0])
  120. continue
  121. self.sentinels[clause[0]].add(i)
  122. self.sentinels[clause[-1]].add(i)
  123. for lit in clause:
  124. self.occurrence_count[lit] += 1
  125. def _find_model(self):
  126. """
  127. Main DPLL loop. Returns a generator of models.
  128. Variables are chosen successively, and assigned to be either
  129. True or False. If a solution is not found with this setting,
  130. the opposite is chosen and the search continues. The solver
  131. halts when every variable has a setting.
  132. Examples
  133. ========
  134. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  135. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  136. ... {3, -2}], {1, 2, 3}, set())
  137. >>> list(l._find_model())
  138. [{1: True, 2: False, 3: False}, {1: True, 2: True, 3: True}]
  139. >>> from sympy.abc import A, B, C
  140. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  141. ... {3, -2}], {1, 2, 3}, set(), [A, B, C])
  142. >>> list(l._find_model())
  143. [{A: True, B: False, C: False}, {A: True, B: True, C: True}]
  144. """
  145. # We use this variable to keep track of if we should flip a
  146. # variable setting in successive rounds
  147. flip_var = False
  148. # Check if unit prop says the theory is unsat right off the bat
  149. self._simplify()
  150. if self.is_unsatisfied:
  151. return
  152. # While the theory still has clauses remaining
  153. while True:
  154. # Perform cleanup / fixup at regular intervals
  155. if self.num_decisions % self.INTERVAL == 0:
  156. for func in self.update_functions:
  157. func()
  158. if flip_var:
  159. # We have just backtracked and we are trying to opposite literal
  160. flip_var = False
  161. lit = self._current_level.decision
  162. else:
  163. # Pick a literal to set
  164. lit = self.heur_calculate()
  165. self.num_decisions += 1
  166. # Stopping condition for a satisfying theory
  167. if 0 == lit:
  168. yield {self.symbols[abs(lit) - 1]:
  169. lit > 0 for lit in self.var_settings}
  170. while self._current_level.flipped:
  171. self._undo()
  172. if len(self.levels) == 1:
  173. return
  174. flip_lit = -self._current_level.decision
  175. self._undo()
  176. self.levels.append(Level(flip_lit, flipped=True))
  177. flip_var = True
  178. continue
  179. # Start the new decision level
  180. self.levels.append(Level(lit))
  181. # Assign the literal, updating the clauses it satisfies
  182. self._assign_literal(lit)
  183. # _simplify the theory
  184. self._simplify()
  185. # Check if we've made the theory unsat
  186. if self.is_unsatisfied:
  187. self.is_unsatisfied = False
  188. # We unroll all of the decisions until we can flip a literal
  189. while self._current_level.flipped:
  190. self._undo()
  191. # If we've unrolled all the way, the theory is unsat
  192. if 1 == len(self.levels):
  193. return
  194. # Detect and add a learned clause
  195. self.add_learned_clause(self.compute_conflict())
  196. # Try the opposite setting of the most recent decision
  197. flip_lit = -self._current_level.decision
  198. self._undo()
  199. self.levels.append(Level(flip_lit, flipped=True))
  200. flip_var = True
  201. ########################
  202. # Helper Methods #
  203. ########################
  204. @property
  205. def _current_level(self):
  206. """The current decision level data structure
  207. Examples
  208. ========
  209. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  210. >>> l = SATSolver([{1}, {2}], {1, 2}, set())
  211. >>> next(l._find_model())
  212. {1: True, 2: True}
  213. >>> l._current_level.decision
  214. 0
  215. >>> l._current_level.flipped
  216. False
  217. >>> l._current_level.var_settings
  218. {1, 2}
  219. """
  220. return self.levels[-1]
  221. def _clause_sat(self, cls):
  222. """Check if a clause is satisfied by the current variable setting.
  223. Examples
  224. ========
  225. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  226. >>> l = SATSolver([{1}, {-1}], {1}, set())
  227. >>> try:
  228. ... next(l._find_model())
  229. ... except StopIteration:
  230. ... pass
  231. >>> l._clause_sat(0)
  232. False
  233. >>> l._clause_sat(1)
  234. True
  235. """
  236. for lit in self.clauses[cls]:
  237. if lit in self.var_settings:
  238. return True
  239. return False
  240. def _is_sentinel(self, lit, cls):
  241. """Check if a literal is a sentinel of a given clause.
  242. Examples
  243. ========
  244. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  245. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  246. ... {3, -2}], {1, 2, 3}, set())
  247. >>> next(l._find_model())
  248. {1: True, 2: False, 3: False}
  249. >>> l._is_sentinel(2, 3)
  250. True
  251. >>> l._is_sentinel(-3, 1)
  252. False
  253. """
  254. return cls in self.sentinels[lit]
  255. def _assign_literal(self, lit):
  256. """Make a literal assignment.
  257. The literal assignment must be recorded as part of the current
  258. decision level. Additionally, if the literal is marked as a
  259. sentinel of any clause, then a new sentinel must be chosen. If
  260. this is not possible, then unit propagation is triggered and
  261. another literal is added to the queue to be set in the future.
  262. Examples
  263. ========
  264. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  265. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  266. ... {3, -2}], {1, 2, 3}, set())
  267. >>> next(l._find_model())
  268. {1: True, 2: False, 3: False}
  269. >>> l.var_settings
  270. {-3, -2, 1}
  271. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  272. ... {3, -2}], {1, 2, 3}, set())
  273. >>> l._assign_literal(-1)
  274. >>> try:
  275. ... next(l._find_model())
  276. ... except StopIteration:
  277. ... pass
  278. >>> l.var_settings
  279. {-1}
  280. """
  281. self.var_settings.add(lit)
  282. self._current_level.var_settings.add(lit)
  283. self.variable_set[abs(lit)] = True
  284. self.heur_lit_assigned(lit)
  285. sentinel_list = list(self.sentinels[-lit])
  286. for cls in sentinel_list:
  287. if not self._clause_sat(cls):
  288. other_sentinel = None
  289. for newlit in self.clauses[cls]:
  290. if newlit != -lit:
  291. if self._is_sentinel(newlit, cls):
  292. other_sentinel = newlit
  293. elif not self.variable_set[abs(newlit)]:
  294. self.sentinels[-lit].remove(cls)
  295. self.sentinels[newlit].add(cls)
  296. other_sentinel = None
  297. break
  298. # Check if no sentinel update exists
  299. if other_sentinel:
  300. self._unit_prop_queue.append(other_sentinel)
  301. def _undo(self):
  302. """
  303. _undo the changes of the most recent decision level.
  304. Examples
  305. ========
  306. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  307. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  308. ... {3, -2}], {1, 2, 3}, set())
  309. >>> next(l._find_model())
  310. {1: True, 2: False, 3: False}
  311. >>> level = l._current_level
  312. >>> level.decision, level.var_settings, level.flipped
  313. (-3, {-3, -2}, False)
  314. >>> l._undo()
  315. >>> level = l._current_level
  316. >>> level.decision, level.var_settings, level.flipped
  317. (0, {1}, False)
  318. """
  319. # Undo the variable settings
  320. for lit in self._current_level.var_settings:
  321. self.var_settings.remove(lit)
  322. self.heur_lit_unset(lit)
  323. self.variable_set[abs(lit)] = False
  324. # Pop the level off the stack
  325. self.levels.pop()
  326. #########################
  327. # Propagation #
  328. #########################
  329. """
  330. Propagation methods should attempt to soundly simplify the boolean
  331. theory, and return True if any simplification occurred and False
  332. otherwise.
  333. """
  334. def _simplify(self):
  335. """Iterate over the various forms of propagation to simplify the theory.
  336. Examples
  337. ========
  338. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  339. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  340. ... {3, -2}], {1, 2, 3}, set())
  341. >>> l.variable_set
  342. [False, False, False, False]
  343. >>> l.sentinels
  344. {-3: {0, 2}, -2: {3, 4}, 2: {0, 3}, 3: {2, 4}}
  345. >>> l._simplify()
  346. >>> l.variable_set
  347. [False, True, False, False]
  348. >>> l.sentinels
  349. {-3: {0, 2}, -2: {3, 4}, -1: set(), 2: {0, 3},
  350. ...3: {2, 4}}
  351. """
  352. changed = True
  353. while changed:
  354. changed = False
  355. changed |= self._unit_prop()
  356. changed |= self._pure_literal()
  357. def _unit_prop(self):
  358. """Perform unit propagation on the current theory."""
  359. result = len(self._unit_prop_queue) > 0
  360. while self._unit_prop_queue:
  361. next_lit = self._unit_prop_queue.pop()
  362. if -next_lit in self.var_settings:
  363. self.is_unsatisfied = True
  364. self._unit_prop_queue = []
  365. return False
  366. else:
  367. self._assign_literal(next_lit)
  368. return result
  369. def _pure_literal(self):
  370. """Look for pure literals and assign them when found."""
  371. return False
  372. #########################
  373. # Heuristics #
  374. #########################
  375. def _vsids_init(self):
  376. """Initialize the data structures needed for the VSIDS heuristic."""
  377. self.lit_heap = []
  378. self.lit_scores = {}
  379. for var in range(1, len(self.variable_set)):
  380. self.lit_scores[var] = float(-self.occurrence_count[var])
  381. self.lit_scores[-var] = float(-self.occurrence_count[-var])
  382. heappush(self.lit_heap, (self.lit_scores[var], var))
  383. heappush(self.lit_heap, (self.lit_scores[-var], -var))
  384. def _vsids_decay(self):
  385. """Decay the VSIDS scores for every literal.
  386. Examples
  387. ========
  388. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  389. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  390. ... {3, -2}], {1, 2, 3}, set())
  391. >>> l.lit_scores
  392. {-3: -2.0, -2: -2.0, -1: 0.0, 1: 0.0, 2: -2.0, 3: -2.0}
  393. >>> l._vsids_decay()
  394. >>> l.lit_scores
  395. {-3: -1.0, -2: -1.0, -1: 0.0, 1: 0.0, 2: -1.0, 3: -1.0}
  396. """
  397. # We divide every literal score by 2 for a decay factor
  398. # Note: This doesn't change the heap property
  399. for lit in self.lit_scores.keys():
  400. self.lit_scores[lit] /= 2.0
  401. def _vsids_calculate(self):
  402. """
  403. VSIDS Heuristic Calculation
  404. Examples
  405. ========
  406. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  407. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  408. ... {3, -2}], {1, 2, 3}, set())
  409. >>> l.lit_heap
  410. [(-2.0, -3), (-2.0, 2), (-2.0, -2), (0.0, 1), (-2.0, 3), (0.0, -1)]
  411. >>> l._vsids_calculate()
  412. -3
  413. >>> l.lit_heap
  414. [(-2.0, -2), (-2.0, 2), (0.0, -1), (0.0, 1), (-2.0, 3)]
  415. """
  416. if len(self.lit_heap) == 0:
  417. return 0
  418. # Clean out the front of the heap as long the variables are set
  419. while self.variable_set[abs(self.lit_heap[0][1])]:
  420. heappop(self.lit_heap)
  421. if len(self.lit_heap) == 0:
  422. return 0
  423. return heappop(self.lit_heap)[1]
  424. def _vsids_lit_assigned(self, lit):
  425. """Handle the assignment of a literal for the VSIDS heuristic."""
  426. pass
  427. def _vsids_lit_unset(self, lit):
  428. """Handle the unsetting of a literal for the VSIDS heuristic.
  429. Examples
  430. ========
  431. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  432. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  433. ... {3, -2}], {1, 2, 3}, set())
  434. >>> l.lit_heap
  435. [(-2.0, -3), (-2.0, 2), (-2.0, -2), (0.0, 1), (-2.0, 3), (0.0, -1)]
  436. >>> l._vsids_lit_unset(2)
  437. >>> l.lit_heap
  438. [(-2.0, -3), (-2.0, -2), (-2.0, -2), (-2.0, 2), (-2.0, 3), (0.0, -1),
  439. ...(-2.0, 2), (0.0, 1)]
  440. """
  441. var = abs(lit)
  442. heappush(self.lit_heap, (self.lit_scores[var], var))
  443. heappush(self.lit_heap, (self.lit_scores[-var], -var))
  444. def _vsids_clause_added(self, cls):
  445. """Handle the addition of a new clause for the VSIDS heuristic.
  446. Examples
  447. ========
  448. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  449. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  450. ... {3, -2}], {1, 2, 3}, set())
  451. >>> l.num_learned_clauses
  452. 0
  453. >>> l.lit_scores
  454. {-3: -2.0, -2: -2.0, -1: 0.0, 1: 0.0, 2: -2.0, 3: -2.0}
  455. >>> l._vsids_clause_added({2, -3})
  456. >>> l.num_learned_clauses
  457. 1
  458. >>> l.lit_scores
  459. {-3: -1.0, -2: -2.0, -1: 0.0, 1: 0.0, 2: -1.0, 3: -2.0}
  460. """
  461. self.num_learned_clauses += 1
  462. for lit in cls:
  463. self.lit_scores[lit] += 1
  464. ########################
  465. # Clause Learning #
  466. ########################
  467. def _simple_add_learned_clause(self, cls):
  468. """Add a new clause to the theory.
  469. Examples
  470. ========
  471. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  472. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  473. ... {3, -2}], {1, 2, 3}, set())
  474. >>> l.num_learned_clauses
  475. 0
  476. >>> l.clauses
  477. [[2, -3], [1], [3, -3], [2, -2], [3, -2]]
  478. >>> l.sentinels
  479. {-3: {0, 2}, -2: {3, 4}, 2: {0, 3}, 3: {2, 4}}
  480. >>> l._simple_add_learned_clause([3])
  481. >>> l.clauses
  482. [[2, -3], [1], [3, -3], [2, -2], [3, -2], [3]]
  483. >>> l.sentinels
  484. {-3: {0, 2}, -2: {3, 4}, 2: {0, 3}, 3: {2, 4, 5}}
  485. """
  486. cls_num = len(self.clauses)
  487. self.clauses.append(cls)
  488. for lit in cls:
  489. self.occurrence_count[lit] += 1
  490. self.sentinels[cls[0]].add(cls_num)
  491. self.sentinels[cls[-1]].add(cls_num)
  492. self.heur_clause_added(cls)
  493. def _simple_compute_conflict(self):
  494. """ Build a clause representing the fact that at least one decision made
  495. so far is wrong.
  496. Examples
  497. ========
  498. >>> from sympy.logic.algorithms.dpll2 import SATSolver
  499. >>> l = SATSolver([{2, -3}, {1}, {3, -3}, {2, -2},
  500. ... {3, -2}], {1, 2, 3}, set())
  501. >>> next(l._find_model())
  502. {1: True, 2: False, 3: False}
  503. >>> l._simple_compute_conflict()
  504. [3]
  505. """
  506. return [-(level.decision) for level in self.levels[1:]]
  507. def _simple_clean_clauses(self):
  508. """Clean up learned clauses."""
  509. pass
  510. class Level:
  511. """
  512. Represents a single level in the DPLL algorithm, and contains
  513. enough information for a sound backtracking procedure.
  514. """
  515. def __init__(self, decision, flipped=False):
  516. self.decision = decision
  517. self.var_settings = set()
  518. self.flipped = flipped