dpll2.py 21 KB

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