(in-package "ICP")
;;------------------------------------------------------------------------------
;; 
;; File:    MICRO-ICP.LISP 
;; Created: 10/20/94
;; Author:  Will Fitzgerald
;; 
;; Description: Micro version of indexed concept parsing.
;; 
;;------------------------------------------------------------------------------

(eval-when (load eval compile)
  (unless (find-package :icp)
    (make-package :icp)))

(in-package :icp)

(use-package :frames)
(use-package :dmap)
(use-package :tables)
(use-package :log)

(export '(def-assoc clear-icp-memory icp information-value
           best-results *icp-results* print-icp-log m-reference-concept m-root
           score target-concept index-concepts ticp))

;;------------------------------------------------------------------------------
;; Data structures for index concepts, target concepts, and their relationships
;;------------------------------------------------------------------------------

;; an index set is a target concept and its associated index concepts

(defclass index-set ()
  ((target-concept :initarg :target-concept :accessor target-concept)
   (indices :initarg :indices :accessor indices)))

(defmethod print-object ((self index-set) stream)
    (print-unreadable-object (self stream :type t :identity t)
      (format stream "~s "  (target-concept self))
      (format stream "~s"(indices self))))

;; Data tables:
;; whether an object is a target concept;
;; from an index to all of the index sets it participates in;
;; from an index to all of the target concepts for which it is associated;
;; from an index to the number of target concepts it's associated with (for
;; calculating information value)

(deftable target-concept-p)
(deftable index->index-sets)
(deftable index->target-concepts)
(deftable index->target-concepts-cardinality)

(defun target-concept-cardinality ()
  "How many target concepts -- for calculating information value"
  (hash-table-count (target-concept-p)))

;;------------------------------------------------------------------------------
;; Installation of index sets.
;;------------------------------------------------------------------------------

(defmethod equal-instance-p ((index-set1 index-set) (index-set2 index-set))
  (and (eql (target-concept index-set1)
            (target-concept index-set2))
       (equal (indices index-set1)
              (indices index-set2))))

(defun set-index->target-concepts-cardinality (index)
  (if (index->target-concepts-cardinality index)
    (incf (index->target-concepts-cardinality index))
    (setf (index->target-concepts-cardinality index) 1)))

(defmethod install ((index-set index-set))
  (with-slots (target-concept indices) index-set
  (dolist (index indices)
    (unless (frame-of index)
      (warn "~S does not name a frame." index))
    (unless (member index (index->target-concepts index))      
      (push target-concept (index->target-concepts index))
      (set-index->target-concepts-cardinality index))
    (pushnew index-set (index->index-sets index) :test 'equal-instance-p))))

(defun add-index-set (target-concept indices)
  (setf (target-concept-p target-concept) t)
  (install (make-instance 'index-set 
             :target-concept target-concept
             :indices indices))
  target-concept)

(defmacro def-assoc (name &rest indices)
  `(progn
     (define-frame ',name '(m-reference-concept) nil)
     (add-index-set ',name ',indices)))


;;------------------------------------------------------------------------------
;; Class for result from the parser
;;------------------------------------------------------------------------------

(defclass icp-result ()
    ((score :initarg :score :initform 0 :accessor score)
     (target-concept :initarg :target-concept :initform nil :accessor target-concept)
     (index-concepts :initarg :index-concepts :initform nil :accessor index-concepts)))

(defmethod print-object ((self icp-result) stream)
  (with-slots (score target-concept index-concepts) self
    (print-unreadable-object (self stream :type t :identity t)
      (format stream "~4,2F ~S ~S" score target-concept index-concepts))))

(defun make-icp-result (score target-concept index-concepts)
  (make-instance 'icp-result
    :score score
    :target-concept target-concept
    :index-concepts index-concepts))

(defmethod score ((result null)) 0)

(defmethod target-concept ((result null)) nil)

(defmethod index-concepts ((result null)) nil)
  
;;------------------------------------------------------------------------------
;; ICP proper
;;------------------------------------------------------------------------------

(defvar *icp-results* () "A place to store the results of the parser")

(defun icp (words &optional (match-fn 'words->indices))
  (setf *icp-results*
        (remove-duplicates 
         (sort (score-index-sets (find-indices words match-fn))
               #'> :key #'score)
         :key 'target-concept :from-end t))
  (first *icp-results*))

(defun best-results (&optional n)
  (if n (first-n *icp-results* n) *icp-results*))

(defun find-indices (words match-fn)
  (funcall match-fn words))

(defun score-index-sets (found-indices)
  (loop for index-set in (candidate-index-sets found-indices)
        collect (make-icp-result
                 (index-set-score index-set found-indices)
                 (target-concept index-set)
                 (indices index-set))))

;;------------------------------------------------------------------------------
;; Find all candidate index sets from the index concepts seen
;;------------------------------------------------------------------------------

(defun candidate-index-sets (found-indices)
  (remove-duplicates
   (loop for index in (all-absts-in found-indices)
         append (index->index-sets index))))

(defun all-absts-in (concepts)
  (remove-duplicates 
   (loop for concept in concepts
         append (all-abstractions (frame-of concept)))))


;;------------------------------------------------------------------------------
;; Calculate the scores for each candidate index set
;; The real work is done by the appraiser functions. INDEX-SET-SCORE
;; just adds them up.  Appraisers with no votes are not called.
;;------------------------------------------------------------------------------

        
(defun index-set-score (index-set found-indices)
  (let ((score 0))
    (map-table #'(lambda (appraiser votes)
                   (unless (zerop votes)
                     (incf score
                           (call-appraiser appraiser 
                                           index-set found-indices))))
               (appraiser-votes))
    (log:record-log (target-concept index-set)
                    "Total score for target ~S  = ~5,3F"
                    (target-concept index-set)  score)
    (log:record-log (target-concept index-set)
                    "Associated index concepts: ~S~%~&~75,,,'-<~>~%" 
                    (indices index-set))
    score))

(defun call-appraiser (appraiser index-set found-indices)
  (let ((score 
         (* (funcall appraiser index-set found-indices)
            (appraiser-weight appraiser))))
    (log:record-log (target-concept index-set)
                    "~A score = ~5,3F (Raw score * ~5,3F weighting)~%"
                    appraiser score (appraiser-weight appraiser))
    score))
  
;;------------------------------------------------------------------------------
;; Information value functions
;;------------------------------------------------------------------------------

(defun probability-of-index (index)
  (let ((cardinality (index->target-concepts-cardinality index)))
    (if (null cardinality) least-positive-short-float 
        (/ cardinality 
           (target-concept-cardinality)))))

(defun information-value (index)
  (- (log (probability-of-index index) 2)))

;;------------------------------------------------------------------------------
;; Appraisers
;;------------------------------------------------------------------------------

;;; An appraiser is a function assigned a non-zero number of votes.
;;; The function should take an index-set and an found-indices and return
;;; a score between 0 and 1 inclusive. The score is then multiplied by
;;; the appraiser's weight, which is the number of votes associated
;;; with the appraiser divided by the total number of votes for all
;;; appraisers. 

;;; (ASSIGN-VOTES name [votes]) => name
;;;   Assigns the given number of votes to an appraiser. If no votes
;;;   are specified, 1 is assumed.

;;; Bookkeeping:
;;;
;;; As votes are assigned, we keep track of the total votes, to speed
;;; up calculating relative weights at parse time.

(deftable appraiser-votes)

(defvar *total-votes* 0
  "Total number of votes for appraisers.")

(defun clear-appraisers ()
  (clear-table (appraiser-votes))
  (setf *total-votes* 0))

(defun assign-votes (name &optional (votes 1))
  (setf (appraiser-votes name) votes)
  (tally-votes)
  name)

(defun tally-votes ()
  (setq *total-votes* 0)
  (map-table #'(lambda (name votes)
                 (declare (ignore name))
                 (incf *total-votes* votes))
             (appraiser-votes)))

(defun appraiser-weight (appraiser)
  (/ (appraiser-votes appraiser) *total-votes*))

;;------------------------------------------------------------------------------
;; Default appraiser functions
;;------------------------------------------------------------------------------

;;; Each of these appraisers compares a given index set against the
;;; pool of indices actually seen in the input:
;;; 
;;;   PREDICTED-SCORE -- how many predicted items were seen?
;;;   UNPREDICTED-SCORE -- how many items were not predicted?
;;;   UNSEEN-SCORE -- how many predicted items were not seen?


(defun predicted-score (index-set found-indices)
  (let* ((predicted (indices index-set))
         (predicted-items (predicted-items found-indices predicted))
         (score
          (/ (summed-value (target-concept index-set) predicted-items 'identity)
             (summed-value (target-concept index-set) predicted 'identity))))
    (log:record-log  (target-concept index-set)
                     "Predicted raw score = ~5,3F (successfully predicted / predicted)"
                     score)
    score))

(defun unpredicted-score (index-set found-indices)
  (let* ((predicted (indices index-set))
         (unpredicted-items (unpredicted-items found-indices predicted))
         (score
          (- 1 (/ (summed-value (target-concept index-set) unpredicted-items 'identity)
                  (summed-value (target-concept index-set) found-indices 'identity)))))
    (log:record-log  (target-concept index-set)
                     "Unpredicted raw score = ~5,3F (1 - unpredicted / seen)"
                     score)
    score))

(defun unseen-score (index-set found-indices)
  (let* ((predicted (indices index-set))
         (unseen-items (unseen-items found-indices predicted))
         (score
          (- 1 (/ (summed-value (target-concept index-set) unseen-items 'identity)
                  (summed-value (target-concept index-set) predicted 'identity)))))
    (log:record-log  (target-concept index-set)
                     "Unseen raw score = ~5,3F (1 - unseen / predicted)"
                     score)
    score))
 
(defun remove-parts (l)
  "remove index concepts that form part of another index concept"
  (remove-if #'(lambda (item) (member item l :test 'part-of))
             l))

(defun predicted-items (seen-set predicted-set)
  (intersection predicted-set seen-set :test 'abst-or-whole-of))

(defun unpredicted-items (seen-set predicted-set)
  (set-difference seen-set predicted-set :test 'spec-or-part-of))

(defun unpredicted-items (seen-set predicted-set)
  (set-difference (remove-parts seen-set) predicted-set :test 'specp))

(defun unseen-items (seen-set predicted-set)
  (set-difference predicted-set seen-set :test 'abst-or-whole-of))


(defun summed-value (base predicted-set fn)
  (let ((val
         (loop for item in predicted-set
               sum (funcall fn (information-value item)))))
    (log:record-log base "Summed value of ~S~:[ using ~A~;~*~] => ~5,3F"
        predicted-set (eql fn 'identity) fn val)
    val))


;;------------------------------------------------------------------------------
;; This is an example of how to write an expection appraiser, although this 
;; isn't used by default.
;;------------------------------------------------------------------------------ 

(defvar *expectations* nil 
  "A list of target concepts predicted")

(defun add-expectation (target-concept)
  (pushnew target-concept *expectations*))

(defun clear-expectations ()
  (setf *expectations* nil))

(defun set-expectations (target-concepts)
  (setf *expectations* target-concepts))

(defun expected-p (target-concept)
  (and (member target-concept *expectations* ) t))

(defun expected-score (index-set found-indices)
  (declare (ignore found-indices))
  (let* ((target-concept (target-concept index-set))
         (found (expected-p target-concept))
         (score (if found 1 0)))
    (log:record-log  target-concept
                     "Expected raw score = ~5,3F (1 if expected, 0 otherwise)"
                     score)
    score))

;;------------------------------------------------------------------------------
;; Weight the appraisers
;;------------------------------------------------------------------------------

(clear-appraisers)

(assign-votes 'predicted-score 2)
(assign-votes 'unpredicted-score)
(assign-votes 'unseen-score)

;;------------------------------------------------------------------------------
;; Clearing memory
;;------------------------------------------------------------------------------

(defun clear-icp-memory ()
  (clear-frame-memory)
  (clear-predictions :all)
  (clear-table (target-concept-p))
  (clear-table (index->target-concepts))
  (clear-table (index->index-sets))
  (clear-table (index->target-concepts-cardinality))
  t)

;;------------------------------------------------------------------------------
;; A DMAP-based index concept recognizer
;;------------------------------------------------------------------------------

(defun words->indices (sent)
  (reset-parser)
  (let (concepts)
    (setf (call-backs 'm-root)
          (list #'(lambda (item start end) 
                    (record-log 'DMAP "DMAP referenced ~S from ~S" 
                                item (subseq sent (1- start) end))
                    (push item concepts))))
    (parse sent)
    (setf concepts (mapcar #'frames::name concepts))
    (record-log 'WORDS->INDICES 
                "~S ~%produced the index pool ~S~%~75,,,'=<~>~%" 
                sent concepts)
    concepts))

;;------------------------------------------------------------------------------
;; Logging  & testing functions
;;------------------------------------------------------------------------------

(defun first-n (sequence n)
  (loop for i from 1 to n
        for el in sequence 
        collect el))

(defun print-icp-log (&optional (n 7) (stream *standard-output*))
  (print-log 'WORDs->INDICES stream)
  (loop for result in (first-n *icp-results* n) 
        doing
        (print-log (target-concept result) stream)))

(defmacro ticp (&rest words)
  `(with-logging
     (icp ',words)
     (print-icp-log)))