symbolic-sat.org 22 KB

Symbolic SAT Solver

Introduction   nonum

CLOSED: [2018-03-23 Fri 21:53]

The following describes and implements a SAT solver for the five most common logical connectives ($\lnot, ∧, ∨, ⇒, ⇔\}$)ithout requiring an expression to be in a specific normal form. It does so using the Tree Rules for Sentential Logic as described in cite:klenk08:_under_symbol_logic. While it is written first and foremost for programmers and computer scientists, it's also an attempt at a well-documented program that would likely be of interest to logicians who seek to understand more about the actual construction of computer programs. The organization is as follows:

  1. Description and implementation of the Solver. Although this builds on things defined later, this section is the bulk of the document and also the most interesting.

  2. A definition of an Expression Classifier. This is used by the solver to help decide what steps to make as it attempts to solve a given instance of the SAT problem.

  3. A description of several utilities used in the Solver. These utilities are not particularly interesting, but do help to abstract away certain common tasks used in the main solver.

  4. A description of how the software is packaged as a library. This in and of itself is rather mundane, however it should help to further demonstrate the organization of software.

Because of the nature of the problem at hand, this is written using the Common Lisp language cite:CommonLi93:online, which is known in the Artificial Intelligence community. It was chosen for three primary reasons: a) the author's familiarity; b) incredible support for symbolic processing; and c) excellent support for several forms of meta-programming, one of which (template-based meta-programming) is used in this program. As this document is in part for those outside of programming cite:CommonLi95:online,seibel05:_pract_common_lisp provide for excelent reference, as does the above mentioned specification.

TOC   ignore

WORKING Solver [1/2]

The solver is built primarily on three components, a driver (which is itself a shim upon a dispatcher), a dispatcher (which selects which expression handler to use based on the type of expression at hand) and expression handlers (which apply the various tree rules).

  (in-package #:symbolic-sat)

  <<expression-handlers>>
  <<driver>>

Driver

CLOSED: [2018-03-18 Sun 10:14]

The driver itself is a simple shim on top of the handler dispatch facility. As such, it takes an expression, adds the expression to a new list (the unhandled expressions list), and creates another list (the current path).

  (defun solve (expression)
    (let ((solution (dispatch-solution (non-atomic-add expression (list))
                                       (list expression))))
      (when (not (null solution))
        (sort-atoms solution))))

WORKING Expression Handlers [2/7]

This is organized as follows: handler storage, a recipe for handler definition, handler dispatch and the the definition of the handlers themselves.

  (defvar *handlers* '())

  <<defining-handlers>>

  <<dispatching-handlers>>

  <<and-handlers>>
  <<or-handlers>>
  <<implication-handlers>>
  <<biconditional-handlers>>
  <<doubled-not-handler>>

Defining Handlers

CLOSED: [2018-03-23 Fri 21:47]

This macro (define-expression-handler) takes two arguments, an expression type and a handler body. It uses the type to create a name for the handler itself, and the defines a function with that name, taking three arguments: the current expression, the list of unchecked expressions and the current path. It then places the handler body in the function, pushes a single cell of the expression type and the handler name onto the handler storage list, and returns the expression type.

  (defmacro define-expression-handler (expression-type &body handler)
    (let ((name (symbolicate 'handle- expression-type)))
      `(progn
         (defun ,name (expression unchecked current-path)
           ,@handler)
         (pushnew '(,expression-type . ,name) *handlers* :key #'first)
         ',expression-type)))

Dispatching Handlers

CLOSED: [2018-03-23 Fri 21:59]

These 11 lines are the most important in the application. This is what's used to cause a handler to be selected and called, or to decide if a solution has been found. This is done by first checking if the list of unchecked expressions is empty, and if so, simply collecting all atoms in the current path. Otherwise, it takes the top expression from unchecked, and rebinds unchecked to the rest of itself, and then looks for a handler. This is done by looking at those available, and if the current expression is of the given type, ending by applying that handler to the current expression.

  (defun dispatch-solution (unchecked current-path)
    (if (null unchecked)
        (collect-atoms current-path)
        (let ((expression (first unchecked))
              (unchecked (rest unchecked)))
          (do* ((pair (first *handlers*) (first remaining))
                (remaining (rest *handlers*) (rest remaining)))
               ((or (null pair)
                   (expression-type-p (car pair) expression))
                (when (not (null pair))
                  (funcall (cdr pair) expression unchecked current-path)))))))

And

  (define-expression-handler sentential-and
    (when (not (any-conflict-p (rest expression) current-path))
      (dispatch-solution (concatenate 'list (collect-non-atoms (rest expression)) unchecked)
                (concatenate 'list (rest expression) current-path))))

  (define-expression-handler not-and
    (let ((expr-list (map 'list (lambda (expr) `(not ,expr)) (rest (second expression))))
          (satp nil))
      (do* ((expr-prime (first expr-list) (first expr-list-prime))
            (expr-list-prime (rest expr-list) (rest expr-list-prime)))
           ((or satp (null expr-prime)) satp)
        (when (not (has-conflict-p expr-prime current-path))
          (setf satp (dispatch-solution (non-atomic-add expr-prime unchecked)
                               (cons expr-prime current-path)))))))

Or

  (define-expression-handler sentential-or
    (let ((satp nil))
      (do ((expr (first (rest expression)) (first expr-list))
           (expr-list (rest (rest expression)) (rest expr-list)))
          ((or satp (null expr)) satp)
        (when (not (has-conflict-p expr current-path))
          (setf satp (dispatch-solution (non-atomic-add expr unchecked)
                               (cons expr current-path)))))))

  (define-expression-handler not-or
    (let ((expr-list (map 'list (lambda (expr) `(not ,expr))
                          (rest (second expression)))))
      (when (not (any-conflict-p expr-list current-path))
        (dispatch-solution (concatenate 'list (collect-non-atoms expr-list) unchecked)
                  (concatenate 'list expr-list current-path)))))

Implication

  (define-expression-handler sentential-implication
    (let ((branch-a `(not ,(second expression)))
          (branch-b (third expression)))
      (cond
        ((not (has-conflict-p branch-a current-path))
         (dispatch-solution (non-atomic-add branch-a unchecked) (cons branch-a current-path)))
        ((not (has-conflict-p branch-b current-path))
         (dispatch-solution (non-atomic-add branch-b unchecked) (cons branch-b current-path)))
        (t nil))))

  (define-expression-handler not-implication
    (let ((the-expr-list (list (second (second expression)) `(not ,(third (second expression))))))
      (when (not (any-conflict-p the-expr-list current-path))
        (dispatch-solution (concatenate 'list (collect-non-atoms the-expr-list) unchecked)
                  (concatenate 'list the-expr-list current-path)))))

Biconditional

  (define-expression-handler sentential-biconditional
    (let* ((branch-a (rest expression))
           (branch-b (map 'list (lambda (expr) `(not ,expr)) branch-a)))
      (cond
        ((not (any-conflict-p branch-a current-path))
         (dispatch-solution (concatenate 'list (collect-non-atoms branch-a) unchecked)
                   (concatenate 'list branch-a current-path)))
        ((not (any-conflict-p branch-b current-path))
         (dispatch-solution (concatenate 'list (collect-non-atoms branch-b) unchecked)
                   (concatenate 'list branch-b current-path)))
        (t nil))))

  (define-expression-handler not-biconditional
    (let ((branch-a (list (second (second expression)) `(not ,(third (second expression)))))
          (branch-b (list `(not ,(second (second expression))) (third (second expression)))))
      (cond
        ((not (any-conflict-p branch-a current-path))
         (dispatch-solution (concatenate 'list (collect-non-atoms branch-a) unchecked)
                   (concatenate 'list branch-a current-path)))
        ((not (any-conflict-p branch-b current-path))
         (dispatch-solution (concatenate 'list (collect-non-atoms branch-b) unchecked)
                   (concatenate 'list branch-b current-path)))
        (t nil))))

Doubled Not

  (define-expression-handler double-not
    (let ((new (second (second expression))))
      (dispatch-solution (non-atomic-add new unchecked) (cons new current-path))))

WORKING Expression Classification [0/8]

  (in-package #:symbolic-sat-classifier)

  (defvar *types* '())

  <<expression-type-definition>>

  <<expression-type-checking>>

  <<not-classification>>
  <<and-classification>>
  <<or-classification>>
  <<implication-classification>>
  <<bicond-classification>>
  <<atom-classification>>

Expression Type Definition

  (defmacro define-expression-type (type-name &rest predicate)
    (check-type type-name symbol)
    (let ((predicate-name (symbolicate type-name '-p)))
      `(progn
         (defun ,predicate-name (expression)
           ,@predicate)
         (pushnew '(,type-name . ,predicate-name) *types* :key #'first :test #'equal)
         (export ',type-name)
         ',type-name)))

Expresion Type Checking

  (defun expression-type-p (expression-type expression)
    (if (eq '* expression-type)
        t
        (funcall (cdr (assoc expression-type *types*))
                 expression)))

Not

  (define-expression-type sentential-not
      (and (listp expression)
         (equal (first expression) 'not)
         (= (length expression) 2)))

  (define-expression-type double-not
      (and (expression-type-p 'sentential-not expression)
         (listp (second expression))
         (expression-type-p 'sentential-not (second expression))))

And

  (define-expression-type sentential-and
      (and (listp expression)
         (equal (first expression) 'and)
         (>= (length (rest expression)) 2)))

  (define-expression-type not-and
      (and (expression-type-p 'sentential-not expression)
         (listp (second expression))
         (expression-type-p 'sentential-and (second expression))))

Or

  (define-expression-type sentential-or
      (and (listp expression)
         (equal (first expression) 'or)
         (>= (length (rest expression)) 2)))

  (define-expression-type not-or
      (and (expression-type-p 'sentential-not expression)
         (listp (second expression))
         (expression-type-p 'sentential-or (second expression))))

Implication

  (define-expression-type sentential-implication
      (and (listp expression)
         (equal 'implies (first expression))
         (= (length (rest expression)) 2)))

  (define-expression-type not-implication
      (and (expression-type-p 'sentential-not expression)
         (listp (second expression))
         (expression-type-p 'sentential-implication (second expression))))

Biconditional

  (define-expression-type sentential-biconditional
      (and (listp expression)
         (equal (first expression) 'iff)
         (= (length (rest expression)) 2)))

  (define-expression-type not-biconditional
      (and (expression-type-p 'sentential-not expression)
         (listp (second expression))
         (expression-type-p 'sentential-biconditional (second expression))))

Other

  (define-expression-type atom
      (or (symbolp expression)
         (and (expression-type-p 'sentential-not expression)
            (symbolp (second expression)))))

WORKING Utilities [0/4]

  (in-package #:symbolic-sat-utils)

  <<clause-collection>>
  <<conflict-checking>>
  <<clause-adding>>
  <<sort-atoms>>

Clause Collection

  (defun collect-atoms (expr-list)
    (remove-duplicates (remove-if (lambda (expr)
                                    (not (expression-type-p 'atom expr)))
                                  expr-list)
                       :test #'equal))

  (defun collect-non-atoms (expr-list)
    (remove-duplicates (remove-if (lambda (expr)
                                    (expression-type-p 'atom expr))
                                  expr-list)
                       :test #'equal))

Conflict Checking

  (defun has-conflict-p (current path)
    (or (member `(not ,current) path :test #'equal)
       (and (expression-type-p 'sentential-not current)
          (member (second current) path :test #'equal))))

  (defun any-conflict-p (expr-list path)
    (do* ((expr (first expr-list) (first expr-list-prime))
          (expr-list-prime (rest expr-list) (rest expr-list-prime)))
         ((or (has-conflict-p expr path)
             (null expr))
          (when (not (null expr))
            t))))

Clause Adding

  (defun non-atomic-add (expr list)
    (if (not (expression-type-p 'atom expr))
        (cons expr list)
        list))

Sort Atoms

  (defun sort-atoms (atoms-list)
    (flet ((atom-less-than-p (atom-a atom-b)
             (let ((aa (if (atom atom-a) atom-a (second atom-a)))
                   (ab (if (atom atom-b) atom-b (second atom-b))))
               (string<= (format nil "~A" aa)
                         (format nil "~A" ab)))))
      (sort atoms-list #'atom-less-than-p)))

Packaging [2/2]

As a software library, packaging is important. In Common Lisp, it's done using system packages and something called ASDF (A System Definition Facility) to enable the library to be loaded correctly and quickly.

Package Definition

CLOSED: [2018-05-28 Mon 10:05]

Symbolic SAT is composed of four packages, as follows:

symbolic-sat-common-symbols

This package exports two symbols that are used and in common for all other packages. These symbols also provide a part of the external API.

symbolic-sat-classifier

This package provides an API for classifying expressions – deciding what type of expression is at hand. It requires the use of the Common Lisp package (providing all basic symbols as defined in the standard), and the previous common symbols package. It also uses symbolicate from Alexandria to mash symbols together for use in automatically generated function and variable names. It directly exports one symbol, expression-type-p which is better described in Section id:34540858-636e-4336-89e7-63dca947c739.

symbolic-sat-utils

Another utility package, the functions exported are well defined in Section id:1c6e6f57-1c3e-4a9f-bd08-6223fc83e4f9, but it builds on the previous two packages, and exports the named symbols.

symbolic-sat

This is the final package, providing perhaps, the most important part of the API, solve. It builds upon all three prior packages, and again uses symbolicate for mashing symbols together.

  (defpackage #:symbolic-sat-common-symbols
    (:use #:cl)
    (:export #:implies
             #:iff))

  (defpackage #:symbolic-sat-classifier
    (:use #:cl
          #:symbolic-sat-common-symbols)
    (:import-from #:alexandria
                  #:symbolicate)
    (:export #:expression-type-p))

  (defpackage #:symbolic-sat-utils
    (:use #:cl
          #:symbolic-sat-common-symbols
          #:symbolic-sat-classifier)
    (:export #:collect-atoms
             #:collect-non-atoms
             #:has-conflict-p
             #:any-conflict-p
             #:non-atomic-add
             #:sort-atoms))

  (defpackage #:symbolic-sat
    (:use #:cl
          #:symbolic-sat-classifier
          #:symbolic-sat-common-symbols
          #:symbolic-sat-utils)
    (:import-from #:alexandria
                  #:symbolicate)
    (:export #:solve))

System Definition

CLOSED: [2018-05-28 Mon 10:16]

This defines the ASDF system, called #:symbolic-sat, having a description, an author, and a license as properties. It states that it depends on #:alexandria, a utilities collection. It further states that the components (as defined by the expression following :components) must be loaded in that order with :serial t.

  (asdf:defsystem #:symbolic-sat
    :description "A basic Symbolic SAT solver using the Tableaux (tree) method for Sentential Logic."
    :author "Samuel Flint <swflint@flintfam.org>"
    :license "GNU GPLv3 or Later"
    :depends-on (#:alexandria)
    :serial t
    :components ((:file "packages")
                 (:file "classifier")
                 (:file "utils")
                 (:file "solver")))

Bibliography   ignore