src/directed-graph.lisp @ b786d38cb2aa
Unfuck the Graphviz labels
author |
Steve Losh <steve@stevelosh.com> |
date |
Sun, 06 Nov 2016 16:16:09 +0000 |
parents |
f5cdc0242ec0 |
children |
1b9b79185f17 |
(in-package :digraph)
;;;; Utils --------------------------------------------------------------------
(defun make-hash-table-portably (&key (size 0) test hash-function)
;; Only try to pass :hash-function if we were given it, so we don't explode in
;; implementations that don't support it.
;;
;; Also, use `apply` instead of a simple `if` because we don't want spurious
;; compiler warnings... This is ugly.
(apply #'make-hash-table :test test :size size
(if hash-function
(list :hash-function hash-function)
'())))
;;;; Data ---------------------------------------------------------------------
(defclass digraph ()
((nodes :initarg :nodes :accessor digraph-nodes)
(test :initarg :test :accessor digraph-test)
(hash-function :initarg :hash-function :accessor digraph-hash-function)))
(defun make-digraph (&key initial-vertices
(test #'eql)
(hash-function nil))
(let ((digraph (make-instance 'digraph
:nodes (make-hash-table-portably
:test test
:size (length initial-vertices)
:hash-function hash-function)
:test test
:hash-function hash-function)))
(mapc (curry #'insert-vertex digraph) initial-vertices)
digraph))
(defmethod print-object ((d digraph) stream)
(print-unreadable-object (d stream :type t :identity t)
(format stream "~:S" (hash-table-keys (digraph-nodes d)))))
(defmacro pred (digraph object)
`(car (gethash ,object (digraph-nodes ,digraph))))
(defmacro succ (digraph object)
`(cdr (gethash ,object (digraph-nodes ,digraph))))
;;;; Basic API ----------------------------------------------------------------
(defun vertices (digraph)
(hash-table-keys (digraph-nodes digraph)))
(defun edges (digraph)
(map-edges #'cons digraph))
(defun predecessors (digraph object)
(copy-list (pred digraph object)))
(defun successors (digraph object)
(copy-list (succ digraph object)))
(defun neighbors (digraph object)
(union (predecessors digraph object)
(successors digraph object)
:test (digraph-test digraph)))
(defun contains-vertex-p (digraph object)
(nth-value 1 (gethash object (digraph-nodes digraph))))
(defun contains-edge-p (digraph predecessor successor)
(ensure-boolean (member successor (succ digraph predecessor)
:test (digraph-test digraph))))
(defun insert-vertex (digraph object)
(nth-value 1 (ensure-gethash object (digraph-nodes digraph)
(cons nil nil))))
(defun insert-edge (digraph predecessor successor)
(assert (contains-vertex-p digraph predecessor) (predecessor)
"Cannot add edge with predecessor ~S because it is not in the graph"
predecessor)
(assert (contains-vertex-p digraph successor) (successor)
"Cannot add edge with successor ~S because it is not in the graph"
successor)
(pushnew predecessor (pred digraph successor) :test (digraph-test digraph))
(pushnew successor (succ digraph predecessor) :test (digraph-test digraph))
(values))
(defun insert-chain (digraph predecessor successor &rest later-successors)
(insert-edge digraph predecessor successor)
(when later-successors
(apply #'insert-chain digraph successor later-successors)))
(defun remove-edge (digraph predecessor successor)
(removef (succ digraph predecessor) successor :test (digraph-test digraph))
(removef (pred digraph successor) predecessor :test (digraph-test digraph))
(values))
(defun remove-vertex (digraph object)
(let ((ps (pred digraph object))
(ss (succ digraph object))
(test (digraph-test digraph)))
(loop :for p :in ps :do (removef (succ digraph p) object :test test))
(loop :for s :in ss :do (removef (pred digraph s) object :test test)))
(remhash object (digraph-nodes digraph))
(values))
(defun degree (digraph object)
(length (neighbors digraph object)))
(defun degree-in (digraph object)
(length (pred digraph object)))
(defun degree-out (digraph object)
(length (succ digraph object)))
(defun size (digraph)
(hash-table-count (digraph-nodes digraph)))
;;;; Iteration ----------------------------------------------------------------
(defmacro do-vertices ((symbol digraph) &body body)
`(loop :for ,symbol :being :the hash-keys :of (digraph-nodes ,digraph)
:do (progn ,@body)))
(defmacro do-edges ((predecessor-symbol successor-symbol digraph) &body body)
(with-gensyms (succs)
`(loop
:for ,predecessor-symbol :being :the hash-keys :of (digraph-nodes ,digraph)
:using (hash-value (nil . ,succs))
:do (loop :for ,successor-symbol :in ,succs ; i miss u, iterate
:do (progn ,@body)))))
(defun mapc-vertices (function digraph)
(do-vertices (v digraph) (funcall function v)))
(defun mapc-edges (function digraph)
(do-edges (p s digraph) (funcall function p s)))
(defun map-vertices (function digraph)
(let ((result nil))
(do-vertices (v digraph) (push (funcall function v) result))
result))
(defun map-edges (function digraph)
(let ((result nil))
(do-edges (p s digraph) (push (funcall function p s) result))
result))
(defun dump (digraph)
(format t "Digraph :TEST ~A~%:CONTENTS " (digraph-test digraph))
(finish-output)
(ql:quickload :losh :silent t)
(funcall (intern "PRINT-HASH-TABLE" (find-package :losh))
(digraph-nodes digraph))
(values))
;;;; Copying ------------------------------------------------------------------
(defun copy-digraph (digraph)
;; todo make this faster, but at least this works
(let ((copy (make-digraph :test (digraph-test digraph)
:initial-vertices (vertices digraph))))
(do-edges (p s digraph) (insert-edge digraph p s))
copy))
;;;; Traversal ----------------------------------------------------------------
;;; Adapted from http://algorithms.wtf/
(defun mapc-depth-first (function digraph start-vertex)
(let ((seen nil))
(labels ((recur (vertex)
(when (not (member vertex seen :test (digraph-test digraph)))
(push vertex seen)
(funcall function vertex)
(mapcar #'recur (succ digraph vertex)))))
(when (contains-vertex-p digraph start-vertex)
(recur start-vertex))))
nil)
(defun mapc-breadth-first (function digraph start-vertex)
(let ((seen nil)
(remaining nil))
(labels ((recur (vertex)
(when (not (member vertex seen :test (digraph-test digraph)))
(push vertex seen)
(funcall function vertex)
;;; todo maybe use jpl queues here...
(appendf remaining (succ digraph vertex)))
(when remaining
(recur (pop remaining)))))
(when (contains-vertex-p digraph start-vertex)
(recur start-vertex))))
nil)
(defun map-depth-first (function digraph start-vertex)
(let ((result nil))
(mapc-depth-first (lambda (v) (push (funcall function v) result))
digraph start-vertex)
(nreverse result)))
(defun map-breadth-first (function digraph start-vertex)
(let ((result nil))
(mapc-breadth-first (lambda (v) (push (funcall function v) result))
digraph start-vertex)
(nreverse result)))
(defun roots (digraph)
(remove-if-not (lambda (v) (null (pred digraph v)))
(vertices digraph)))
(defun leafs (digraph)
(remove-if-not (lambda (v) (null (succ digraph v)))
(vertices digraph)))
(defun mapc-topological (function digraph)
(let ((status (make-hash-table-portably
:test (digraph-test digraph)
:hash-function (digraph-hash-function digraph))))
(labels
((visit (vertex)
(ecase (gethash vertex status :new)
(:active
(error "Cycle detected during topological map involving vertex ~S"
vertex))
(:new (recur vertex))
(:done nil)))
(recur (vertex)
(setf (gethash vertex status) :active)
(mapc #'visit (succ digraph vertex))
(setf (gethash vertex status) :done)
(funcall function vertex)))
(mapc #'visit (roots digraph))))
nil)
(defun map-topological (function digraph)
(let ((result nil))
(mapc-topological (lambda (v) (push (funcall function v) result)) digraph)
(nreverse result)))
;;;; Scratch ------------------------------------------------------------------
(defun make-test-digraph ()
;; a ----> middle ----> z ORPHAN
;; ^ ^ ^
;; | | |
;; B ---------+ |
;; | | +-------------------+
;; v | | v
;; c --------> dogs FOO ----> bar ----> baz
;; ^ |
;; | |
;; +------------------------+
(let ((g (make-digraph
:initial-vertices
'(a b c dogs middle z orphan foo bar baz))))
(insert-edge g 'a 'middle)
(insert-edge g 'b 'middle)
(insert-edge g 'b 'a)
(insert-edge g 'middle 'z)
; (insert-edge g 'z 'z)
(insert-edge g 'b 'c)
(insert-edge g 'c 'dogs)
(insert-edge g 'dogs 'middle)
; (insert-edge g 'dogs 'c)
(insert-edge g 'foo 'baz)
(insert-edge g 'foo 'bar)
(insert-edge g 'bar 'baz)
g))
#+scratch
(progn
(defparameter *d* (make-test-digraph))
(setf cl-dot:*dot-path* "/usr/local/bin/dot")
(digraph.dot:draw *d*))