Coverage report: /home/ellis/comp/core/lib/obj/graph.lisp
Kind | Covered | All | % |
expression | 64 | 637 | 10.0 |
branch | 6 | 46 | 13.0 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; lib/obj/graph.lisp --- Graphs
3
;; Graph objects and algorithms
7
;; Mostly modeled off of eschulte's GRAPH library - see also DAT/DOT
9
;; ref: https://eschulte.github.io/graph/
11
;; Our goals are slightly different than the original library - we prioritize
12
;; flexibility over speed or code size. To this end the base GRAPH class
13
;; accepts NODE vectors in addition to hash-tables. We also make minimal
14
;; assumptions about the underling node types - it is a blank class
15
;; definition. Edges are a bit more complicated - they subclass ID which can be
18
(in-package :obj/graph)
23
(defclass vertex (id node)
25
(:documentation "generic vertex mixin. The difference between this class and NODE is
26
that a vertex always carries an ID slot."))
30
((in :initarg :in :accessor edge-in) (out :initarg :out :accessor edge-out))
31
(:documentation "generic edge mixin. Compatible with the NODE and ID protocols."))
33
(defmethod name ((self edge))
34
(cons (edge-in self) (edge-out self)))
36
(defclass edgex (edge id)
38
(:documentation "Edge compatible with the NODE and ID protocols."))
40
(defclass directed-edge (edge)
42
(:documentation "An edge with an implicit direction from node A to B."))
44
(defclass weighted-edge (edge)
45
((weight :initform 1d0 :initarg :weight :accessor weight-of)))
48
(defun copy-hash (hash &optional test comb)
49
"Return a copy of HASH.
50
Optional argument TEST specifies a new equality test to use for the
51
copy. Second optional argument COMB specifies a function to use to
52
combine the values of elements of HASH which collide in the copy due
53
to a new equality test specified with TEST."
54
(let ((comb (when comb (fdefinition comb)))
55
(copy (make-hash-table :test (or test (hash-table-test hash)))))
56
(maphash (lambda (k v) (setf (gethash k copy)
57
(if (and (gethash k copy) comb)
58
(funcall comb (gethash k copy) v)
63
(defun node-hash-equal (hash1 hash2)
64
"Test node hashes HASH1 and HASH2 for equality."
65
(set-equal (hash-table-alist hash1)
66
(hash-table-alist hash2)
68
(and (equalp (car a) (car b))
69
(set-equal (cdr a) (cdr b) :test 'tree-equal)))))
71
(defun edge-hash-equal (hash1 hash2)
72
"Test edge hashes HASH1 and HASH2 for equality."
73
(set-equal (hash-table-alist hash1)
74
(hash-table-alist hash2)
77
(defun edge-equalp (edge1 edge2)
78
(set-equal edge1 edge2 :test 'equal))
80
(defun directed-edge-equalp (edge1 edge2)
81
(tree-equal edge1 edge2))
83
(defun sxhash-edge (edge)
84
(sxhash (sort (copy-tree edge)
86
((and (numberp (car edge)) (numberp (cdr edge)))
88
(or (< (imagpart a) (imagpart b))
89
(and (= (imagpart a) (imagpart b))
90
(< (realpart a) (realpart b))))))
91
((or (numberp (car edge)) (numberp (second edge)))
92
(lambda (a b) (declare (ignore a b)) t))
95
(sb-ext:define-hash-table-test edge-equalp sxhash-edge)
97
(sb-ext:define-hash-table-test directed-edge-equalp sxhash)
100
(defgeneric nodes (graph))
101
(defgeneric (setf nodes) (graph nodes))
102
(defgeneric edges (graph))
103
(defgeneric (setf edges) (graph edges))
105
(defgeneric graph-equal (graph1 graph2))
107
(defgeneric subgraph (graph nodes)
108
(:documentation "Return the subgraph of GRAPH restricted to NODES."))
110
(defgeneric delete-node (graph node)
111
(:documentation "Delete NODE from GRAPH.
112
Delete and return the old edges of NODE in GRAPH."))
114
(defgeneric has-node-p (graph node)
115
(:documentation "Return non-nil if GRAPH has node NODE."))
116
(defgeneric has-edge-p (graph edge)
117
(:documentation "Return `true' if GRAPH has edge EDGE."))
119
(defgeneric edge-weight (edge &key &allow-other-keys)
120
(:method ((edge t) &key &allow-other-keys) (values 1.0)))
122
(defgeneric edge-value (graph edge)
123
(:method ((graph t) (edge t)) (values nil)))
125
(defgeneric (setf edge-value) (new graph edge))
127
(defgeneric delete-edge (graph edge)
128
(:documentation "Delete EDGE from GRAPH.
129
Return the old value of EDGE."))
131
(defgeneric node-edges (graph node)
132
(:documentation "Return the edges of NODE in GRAPH."))
134
(defgeneric (setf node-edges) (new graph node)
135
(:documentation "Set the edges of NODE in GRAPH to NEW.
136
Delete and return the old edges of NODE in GRAPH."))
138
(defgeneric add-node (graph node))
140
(defgeneric add-edge (graph edge &optional value))
143
(defclass graph (node)
144
((nodes :initform (make-hash-table :test 'equal)
145
:type (or (vector node) hash-table)
148
(edges :initform (make-hash-table :test 'edge-equalp)
152
(:documentation "generic graph object."))
154
(defmethod copy-graph ((graph graph))
155
(make-instance (type-of graph) :nodes (copy-hash (nodes graph)) :edges (copy-hash (edges graph))))
157
(defmethod subgraph ((graph graph) nodes)
158
(make-instance (type-of graph) :nodes nodes :edges (copy-hash (edges graph))))
160
(defmethod has-edge-p ((graph graph) edge)
161
(multiple-value-bind (value included) (gethash edge (edges graph))
162
(declare (ignorable value)) included))
164
(defmethod has-node-p ((graph graph) node)
165
(multiple-value-bind (value included) (gethash node (nodes graph))
166
(declare (ignorable value)) included))
168
(defmethod delete-node ((graph graph) node)
169
(prog1 (mapcar (lambda (edge) (cons edge (delete-edge graph edge)))
170
(node-edges graph node))
171
(remhash node (nodes graph))))
173
(defmethod delete-edge ((graph graph) edge)
174
(prog1 (edge-value graph edge)
175
(mapc (lambda (node) (setf (gethash node (nodes graph))
176
(remove edge (gethash node (nodes graph))
177
:test 'edge-equalp)))
179
(remhash edge (edges graph))))
181
(defmethod node-edges ((graph graph) node)
182
(multiple-value-bind (edges included) (gethash node (nodes graph))
183
(assert included (node graph) "~S doesn't include ~S" graph node)
186
(defmethod (setf node-edges) (new (graph graph) node)
187
(prog1 (mapc {delete-edge graph} (gethash node (nodes graph)))
188
(mapc {add-edge graph} new)))
190
(defmethod add-edge ((graph graph) edge &optional value)
192
(add-node graph node)
193
(pushnew (case (type-of graph)
194
(graph (remove-duplicates edge))
195
(directed-graph edge))
196
(gethash node (nodes graph))
199
(setf (gethash edge (edges graph)) value)
202
(defmethod edge-value ((graph graph) edge)
203
(multiple-value-bind (value included) (gethash edge (edges graph))
204
(assert included (edge graph) "~S doesn't include ~S" graph edge)
207
(defmethod (setf edge-value) (new (graph graph) edge)
208
(setf (gethash edge (edges graph)) new))
210
(defgeneric merge-nodes (graph node1 node2 &key new)
211
(:documentation "Combine NODE1 and NODE2 in GRAPH into the node NEW.
212
All edges of NODE1 and NODE2 in GRAPH will be combined into a new node
213
of value NEW. Edges between only NODE1 and NODE2 will be removed."))
215
(defmethod merge-nodes ((graph graph) node1 node2 &key (new node1))
216
;; replace all removed edges with NEW instead of NODE1 or NODE2
219
(destructuring-bind(edge . value) l
220
(let ((e (mapcar (lambda (n) (if (member n (list node1 node2)) new n)) edge)))
221
(if (has-edge-p graph e)
222
(when (and (edge-value graph e) value)
223
(setf (edge-value graph e) (+ (edge-value graph e) value)))
224
(add-edge graph e value)))))
225
;; drop edges between only node1 and node2
226
(remove-if-not [{set-difference _ (list node1 node2)} #'car]
227
;; delete both nodes keeping their edges and values
228
(prog1 (append (delete-node graph node1)
229
(delete-node graph node2))
231
(add-node graph new))))
234
(defgeneric merge-edges (graph edge1 edge2 &key value)
235
(:documentation "Combine EDGE1 and EDGE2 in GRAPH into a new EDGE.
236
Optionally provide a value for the new edge, the values of EDGE1 and
237
EDGE2 will be combined."))
239
(defmethod merge-edges ((graph graph) edge1 edge2 &key value)
240
(add-edge graph (remove-duplicates (append edge1 edge2))
242
(when (and (edge-value graph edge1) (edge-value graph edge2))
243
(+ (edge-value graph edge1) (edge-value graph edge2)))))
244
(append (delete-edge graph edge1)
245
(delete-edge graph edge2)))
247
(defgeneric degree (graph node)
248
(:documentation "Return the degree of NODE in GRAPH."))
250
(defmethod degree ((graph graph) node)
251
(length (node-edges graph node)))
253
(defmethod add-node ((graph graph) node)
254
;; NOTE: This is where our implementation breaks character from Eschulte's
255
;; implementation. We currently accept strings in addition to numbers and symbols.
256
(assert (or (numberp node) (symbolp node) (stringp node)) (node)
257
"Nodes must be numbers, symbols or keywords, not ~S.~%Invalid node:~S"
259
(unless (has-node-p graph node)
260
(setf (gethash node (nodes graph)) nil)
264
(defclass directed-graph (graph)
265
((edges :initform (make-hash-table :test 'directed-edge-equalp)
266
:type (or (vector directed-edge) hash-table)
269
(:documentation "graph with only directed edges."))
271
(defgeneric indegree (digraph node)
272
(:documentation "The number of edges directed to NODE in GRAPH."))
274
(defmethod indegree ((digraph directed-graph) node)
275
(length (remove-if-not [{member node} #'cdr] (node-edges digraph node))))
277
(defgeneric outdegree (digraph node)
278
(:documentation "The number of edges directed from NODE in DIGRAPH."))
280
(defmethod outdegree ((digraph directed-graph) node)
281
(length (remove-if-not [{equal node} #'car] (node-edges digraph node))))
284
(defgeneric shortest-path (graph a b &optional heuristic)
285
(:documentation "Return the shortest path in GRAPH from A to B.
286
Implemented using A* search. Optional argument HEURISTIC may be a
287
function which returns an estimated heuristic cost from an node to the
288
target B. The default value for HEURISTIC is the constant function of
289
0, reducing this implementation to Dijkstra's algorithm. The
290
HEURISTIC function must satisfy HEURITIC(x)≤d(x,y)+HEURITIC(y) ∀ x,y
291
in GRAPH allowing the more efficient monotonic or \"consistent\"
292
implementation of A*.")
293
(:method ((graph graph) a b
295
(heuristic (constantly 0))
297
(from (make-hash-table))
298
(fringe (sb-concurrency:make-queue))
299
(open (make-hash-table))
300
(closed (make-hash-table))
301
(g (make-hash-table))
302
(f (make-hash-table)))
303
(when (equal a b) (return-from shortest-path nil))
304
(labels ((reconstruct-path (current)
305
(destructuring-bind (node . edge) (gethash current from)
306
(cons edge (unless (member a edge) (reconstruct-path node))))))
307
(setf (gethash a g) 0
308
(gethash a f) (funcall heuristic a)
311
(sb-concurrency:enqueue fringe (gethash a f))
313
(do ((current (sb-concurrency:dequeue fringe) (sb-concurrency:dequeue fringe)))
314
((zerop (hash-table-count open))
315
(multiple-value-bind (value present-p) (gethash b f)
317
(values (nreverse (reconstruct-path b)) value))))
319
(when (eql current b)
320
(return-from shortest-path
321
(values (nreverse (reconstruct-path current))
322
(gethash current f))))
324
(remhash current open)
325
(setf (gethash current closed) t)
328
(let ((weight (or (edge-value graph edge) 1)))
330
(unless (gethash next closed)
331
(setf (gethash next open) t)
332
(let ((tentative (+ (gethash current g) weight)))
333
(multiple-value-bind (value present-p)
335
(when (or (not present-p)
337
(setf (gethash next from) (cons current edge)
338
(gethash next g) tentative
340
(+ tentative (funcall heuristic next)))
341
(sb-concurrency:enqueue fringe (gethash next f)))))))
343
(directed-graph (cdr (member current edge)))
344
(graph (remove current edge))))))
345
(node-edges graph current))))))
349
;; Stoer, M. and Wagner, Frank. 1997. A Simple Min-Cut Algorithm.
350
;; Journal of the ACM
352
;; Theorem: Let s,t ∈ (nodes G), let G' be the result of merging s and
353
;; t in G. Then (min-cut G) is equal to the minimum of the
354
;; min cut of s and t in G and (min-cut G').
356
(defun weigh-cut (graph cut)
357
(reduce #'+ (mapcar {edge-value graph}
358
(remove-if-not (lambda (edge)
359
(and (intersection edge (first cut))
360
(intersection edge (second cut))))
363
(defgeneric min-cut (graph)
365
"Return both the global min-cut of GRAPH and the weight of the cut."))
367
(defmethod min-cut ((graph graph))
368
(let ((g (copy-graph graph))
369
(merged-nodes (mapcar (lambda (n) (list n n)) (nodes graph)))
371
(flet ((connection-weight (group node)
372
;; return the weight of edges between GROUP and NODE
373
(reduce #'+ (mapcar {edge-value g}
374
(remove-if-not {intersection group}
375
(node-edges g node)))))
377
;; merge in the graph
379
;; update our merged nodes alist
380
(setf (cdr (assoc a merged-nodes))
381
(append (cdr (assoc a merged-nodes))
382
(cdr (assoc b merged-nodes))))
384
(remove-if (lambda (it) (eql (car it) b)) merged-nodes))))
385
(loop :while (> (length (nodes g)) 1) :do
386
(let* ((a (list (random (nodes g))))
387
(rest (remove (car a) (nodes g))))
388
(loop :while rest :do
389
;; grow A by adding the node most tightly connected to A
390
(let ((new (car (sort rest #'> :key {connection-weight a}))))
391
(setf rest (remove new rest))
393
;; store the cut-of-phase
394
(push (cons (connection-weight (cdr a) (car a))
395
(cdr (assoc (car a) merged-nodes)))
397
;; merge two last added nodes
398
(my-merge (first a) (second a))))
399
;; return the minimum cut-of-phase
400
(let* ((half (cdar (sort cuts-of-phase #'< :key #'car)))
401
(cut (list half (set-difference (nodes graph) half))))
402
(values (sort cut #'< :key #'length) (weigh-cut graph cut))))))
404
;; https://en.wikipedia.org/wiki/Degeneracy_(graph_theory)