src/reasoners/zdd.lisp @ 41b2461432fc
Implement the basic iset traversal logic
author |
Steve Losh <steve@stevelosh.com> |
date |
Thu, 15 Dec 2016 13:03:18 -0500 |
parents |
51bc78b22d98 |
children |
4843f09b50f6 |
(in-package :scully.reasoners.zdd)
;;;; Utils --------------------------------------------------------------------
(defmacro defclass* (name-and-options direct-superclasses slots &rest options)
(flet ((slot-definition (conc-name slot)
(destructuring-bind (name &key
type
documentation
initform
(accessor (symb conc-name name))
(initarg (intern (symbol-name name) :keyword)))
(ensure-list slot)
`(,name
:initarg ,initarg
:accessor ,accessor
,@(when initform `(:initform ,initform))
,@(when type `(:type ,type))
,@(when documentation `(:documentation ,documentation))))))
(destructuring-bind (name &key (conc-name (symb name '-)))
(ensure-list name-and-options)
`(defclass ,name ,direct-superclasses
,(mapcar (curry #'slot-definition conc-name) slots)
,@options))))
;;;; Rule Forests -------------------------------------------------------------
(defclass* (rule-forest :conc-name rf-) ()
(strata
upper-bound
lower-bound))
(defun make-forest-with (old-forest strata)
(make-instance 'rule-forest
:strata strata
:upper-bound (rf-upper-bound old-forest)
:lower-bound (rf-lower-bound old-forest)))
(defun forest-empty-p (forest)
(null (rf-strata forest)))
(defun find-bound (predicate layer)
(extremum (mapcar #'scully.gdl::rule-head layer) predicate))
(defun find-lower-bound (layer)
(find-bound #'< layer))
(defun find-upper-bound (layer)
(find-bound #'> layer))
;;;; Reasoner -----------------------------------------------------------------
(defclass* (zdd-reasoner :conc-name zr-) ()
(rules
roles
term->number
number->term
initial-zdd
legal-zdd
goal-zdd
terminal-zdd
possible-forest
happens-forest))
(defun find-initial-state (rules term->number)
(-<> rules
(mapcan (lambda-match
((list (list* 'ggp-rules::init body))
`((ggp-rules::true ,@body))))
<>)
(mapcar (lambda (term) (gethash term term->number)) <>)))
(defun find-roles (rules)
(mapcan (lambda-match
((list (list 'ggp-rules::role r))
(list r)))
rules))
(defun make-predicate-zdd (predicate term->number)
(-<> term->number
hash-table-alist
(remove-if-not (lambda (rule)
(eql predicate (first (first rule))))
<>)
(mapcar #'cdr <>)
(zdd-set <>)))
(defun make-stratum-rule-trees (stratum)
(-<> stratum
(group-by #'car <>)
hash-table-values
(mapcar #'scully.rule-trees::make-rule-tree <>)))
(defun make-rule-forest (rule-layers layer)
(let ((rules (gethash layer rule-layers)))
(make-instance 'rule-forest
:strata (-<> rules
scully.terms::stratify-layer
(mapcar #'make-stratum-rule-trees <>))
:upper-bound (find-upper-bound rules)
:lower-bound (find-lower-bound rules))))
(defun make-zdd-reasoner (rules)
"Turn a set of grounded GDL rules into a logic manager.
A rule forest is a collection of individual rule trees in a single layer,
stratified as necessary:
POSSIBLE: (STRATUM-1 STRATUM-2 ...)
HAPPENS: (STRATUM-1 STRATUM-2 ...)
|| ||
|| \/
|| (rule-tree-1 rule-tree-2 ...)
\/
(rule-tree-1 rule-tree-2 ...)
"
(let ((rules (scully.gdl::normalize-rules rules)))
(destructuring-bind (term->number number->term rule-layers)
(scully.terms::integerize-rules rules)
(with-zdd
(make-instance 'zdd-reasoner
:rules rules
:roles (find-roles rules)
:possible-forest (make-rule-forest rule-layers :possible)
:happens-forest (make-rule-forest rule-layers :happens)
:initial-zdd (zdd-set (find-initial-state rules term->number))
:legal-zdd (make-predicate-zdd 'ggp-rules::legal term->number)
:goal-zdd (make-predicate-zdd 'ggp-rules::goal term->number)
:terminal-zdd (make-predicate-zdd 'ggp-rules::terminal term->number)
:term->number term->number
:number->term number->term)))))
;;;; Basic API ----------------------------------------------------------------
(defun number-to-term (reasoner number)
(gethash number (zr-number->term reasoner)))
(defun term-to-number (reasoner term)
(gethash term (zr-term->number reasoner)))
(defun initial-iset (reasoner)
"Return the initial information set of the game."
(zr-initial-zdd reasoner))
(defun rand-state (reasoner iset)
"Select a random member of the given information set."
(mapcar (curry #'number-to-term reasoner)
(zdd-random-member iset)))
(defun terminalp (reasoner iset)
"Return whether the given information set is a terminal state."
(-<> iset
(zdd-meet <> (zr-terminal-zdd reasoner))
zdd-unit-p
not))
(defun roles (reasoner)
(zr-roles reasoner))
;;;; Drawing ------------------------------------------------------------------
(defun label (reasoner n)
(let ((*package* (find-package :ggp-rules)))
(-<> n
(number-to-term reasoner <>)
(structural-string <>))))
(defun draw-zdd (reasoner zdd)
(scully.graphviz::draw-zdd zdd :label-fn (curry #'label reasoner)))
(defun draw-rule-tree (reasoner rule-tree)
(scully.graphviz::draw-rule-tree rule-tree :label-fn (curry #'label reasoner)))
;;;; Logic Application --------------------------------------------------------
(defun tree-to-result (tree)
(adt:match scully.rule-trees::rule-tree tree
((scully.rule-trees::top head) (values nil head))
(scully.rule-trees::bottom (values nil nil))
((scully.rule-trees::node _ _ _) (values tree nil))))
(defun process-stratum (function stratum)
"Process the stratum with `function`.
Two values will be returned:
1. The new stratum (possibly NIL).
2. Any new heads to add (possible NIL).
"
(iterate
(for tree :in stratum)
(for (values new-tree new-head) = (funcall function tree))
(when new-tree (collect new-tree :into new-stratum))
(when new-head (collect new-head :into new-heads))
(finally (return (values new-stratum new-heads)))))
(defun process-forest (function forest)
"Process the rule forest with `function`.
Two values will be returned:
1. The new forest (possibly NIL).
2. Any new heads to add (possible NIL).
"
(iterate
(for stratum :in (rf-strata forest))
(for (values new-stratum new-heads) = (process-stratum function stratum))
(when new-stratum (collect new-stratum :into new-strata))
(appending new-heads :into heads)
(finally (return (values (make-forest-with forest new-strata) heads)))))
(defun advance-tree (tree term)
"Advance the rule tree up to (but not beyond) `term`.
Two values will be returned:
1. Either the resulting rule tree, or NIL if it was advanced down to a sink.
2. The new head if it was advanced down to a TOP sink, or NIL otherwise.
"
(adt:match scully.rule-trees::rule-tree tree
((scully.rule-trees::node term% _ lo)
(if (< term% term)
(advance-tree lo term)
(tree-to-result tree)))
(_ (tree-to-result tree))))
(defun advance-forest (forest term)
"Advance the rule forest up to (but not beyond) `term`.
Two values will be returned:
1. The new forest (possibly NIL).
2. Any new heads to add (possible NIL).
"
(process-forest (rcurry #'advance-tree term) forest))
(defun split-tree-hi (tree term)
(adt:match scully.rule-trees::rule-tree tree
((scully.rule-trees::node term% hi _)
(if (= term% term)
(tree-to-result hi)
(tree-to-result tree)))
(_ (error "Cannot split rule tree: ~S" tree))))
(defun split-tree-lo (tree term)
(adt:match scully.rule-trees::rule-tree tree
((scully.rule-trees::node term% _ lo)
(if (= term% term)
(tree-to-result lo)
(tree-to-result tree)))
(_ (error "Cannot split rule tree: ~S" tree))))
(defun split-forest-hi (forest term)
(process-forest (rcurry #'split-tree-hi term) forest))
(defun split-forest-lo (forest term)
(process-forest (rcurry #'split-tree-lo term) forest))
(defun finalize-heads (reasoner forest heads)
"Finalize the set of heads to add and return the appropriate ZDD."
(prl reasoner forest heads)
(zdd-set heads))
(defun traverse-iset (reasoner iset forest)
"Walk down the information set and rule forest in parallel."
(declare (ignorable reasoner))
(recursively ((iset iset)
(forest forest)
(heads '()))
(ematch iset
;; If we hit an empty sink we're out of sets to ever cons the heads onto,
;; so we can just bail immediately.
((sink nil) iset)
;; If we hit a unit sink we're done with the state-walking portion of this
;; algorithm and can move on the the fixed-pointing of the heads.
((sink t) (finalize-heads reasoner forest heads))
;; Otherwise we need to build a new ZDD node with the recursive results.
((node term hi lo)
(multiple-value-bind*
(((forest advanced-heads) (advance-forest forest term))
((forest-hi hi-heads) (split-forest-hi forest term))
((forest-lo lo-heads) (split-forest-lo forest term)))
(zdd-node
term
(recur hi forest-hi (append heads advanced-heads hi-heads))
(recur lo forest-lo (append heads advanced-heads lo-heads))))))))
(defun apply-rule-forest (reasoner iset forest)
"Apply `forest` to the given information set for `reasoner`."
(declare (ignorable reasoner))
(with-zdd
(traverse-iset reasoner iset forest)))
;;;; Scratch ------------------------------------------------------------------
(defparameter *rules*
(scully.gdl::read-gdl "gdl/tictactoe-grounded.gdl")
; (scully.gdl::read-gdl "gdl/hanoi-grounded.gdl")
; (scully.gdl::read-gdl "gdl/8puzzle-grounded.gdl")
; (scully.gdl::read-gdl "gdl/roshambo2-grounded.gdl")
)
(defparameter *l* (make-zdd-reasoner *rules*))
; (draw-zdd *l* (initial-iset *l*))
; (-<> *l*
; (apply-rule-forest <> (initial-iset *l*) (zr-possible-forest *l*))
; (draw-zdd *l* <>)
; (no <>)
; )