a-star.scm 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. (library (a-star)
  2. (export A*)
  3. (import (except (rnrs base)
  4. let-values
  5. map)
  6. (only (guile)
  7. lambda* λ
  8. when
  9. unless
  10. simple-format
  11. string<?
  12. call-with-output-string)
  13. (ice-9 pretty-print)
  14. (srfi srfi-1) ; lists
  15. (srfi srfi-69) ; hash tables
  16. (srfi srfi-11) ; let-values
  17. ;; Functional Sets
  18. (pfds sets)
  19. ;; Priority Search Queues
  20. (pfds psqs)
  21. (priority-search-queues)
  22. ;; Bounded Balance Trees
  23. (pfds bbtrees)
  24. (graph-model))
  25. (define A*
  26. (lambda* (start
  27. target
  28. node-names
  29. nodes-table
  30. get-neighbor-distance
  31. cost-heuristic
  32. distance<)
  33. (let ([fringe (psq-set (make-psq string<? <)
  34. ;; Initially put the start node in the open-set,
  35. ;; since we need at least some node to go on from.
  36. (node-name start)
  37. ;; The start node has priority 0, which is the
  38. ;; highest priority. It does not really matter,
  39. ;; since there is only one element in the open-set.
  40. 0)]
  41. ;; routes stores the node preceding any target node on the cheapest
  42. ;; path to that target node. Initially only the preceding node for
  43. ;; the start node is set. The start node itself.
  44. [routes (alist->hash-table (list (cons (node-name start)
  45. (node-name start)))
  46. string=?)]
  47. ;; score stores the cost of the cheapest path from the start node to
  48. ;; other nodes as currently known. Initially it is only set for the
  49. ;; start node, as one does not know other costs yet.
  50. [cheapest-path-costs
  51. (alist->hash-table `((,(node-name start) . 0)) string=?)]
  52. ;; Also keep track of a best estimate (calculated using the
  53. ;; heuristic) of the cost from the start node to the target node via
  54. ;; a node. Initially we have not explored any other nodes than the
  55. ;; start node, so the cost for any path via them to the target node
  56. ;; is pessimistically estimated to be infinite. Update formula:
  57. ;; via-node-cost-estimate(node) := current-best-score(node) + heuristic(node).
  58. [via-node-cost-estimate (make-hash-table string=?)])
  59. ;; Set cost estimate for start node.
  60. (for-each (λ (name)
  61. (hash-table-set! via-node-cost-estimate
  62. name
  63. +inf.0))
  64. node-names)
  65. (for-each (λ (name)
  66. (unless (string=? name (node-name start))
  67. (hash-table-set! cheapest-path-costs
  68. name
  69. +inf.0)))
  70. node-names)
  71. (hash-table-set! via-node-cost-estimate
  72. (node-name start)
  73. (cost-heuristic start target))
  74. (simple-format #t "search begins\n")
  75. (let iter
  76. (;; The current-node-name° is the one, that is estimated
  77. ;; to have the lowest cost, when a path to the target
  78. ;; contains it. Initially this should be the start node,
  79. ;; because there is only one node in the fringe.
  80. [current-node-name° (psq-min fringe)]
  81. [fringe° fringe]
  82. [visited° (set-insert (make-set string<?) (node-name start))])
  83. (simple-format #t "fringe: ~a\n" (psq->list fringe°))
  84. (simple-format #t "current node: ~a\n" current-node-name°)
  85. (cond
  86. ;; If the current node is the target node, return the routes.
  87. [(string=? current-node-name° (node-name target))
  88. (simple-format
  89. #t "reached the target, returning routes:\n~a\n"
  90. (call-with-output-string
  91. (λ (port)
  92. (pretty-print (hash-table->alist routes) port))))
  93. routes]
  94. [else
  95. (let ([neighbor-names
  96. (node-neighbors (hash-table-ref nodes-table current-node-name°))])
  97. ;; Per neighbor node update the following: routes, cheapest path
  98. ;; cost, via node path cost estimate
  99. (for-each (λ (neighbor-name)
  100. ;; (cond
  101. ;; ;; [(set-member? visited° neighbor-name)
  102. ;; ;; (simple-format
  103. ;; ;; #t "node ~a is already visited ignoring it as a neighbor\n"
  104. ;; ;; neighbor-name)
  105. ;; ;; (let-values ([(_node-name-of-min fringe-without-current) (psq-pop fringe°)])
  106. ;; ;; (iter fringe-without-current visited°))]
  107. ;; [else ...])
  108. (let* ([distance
  109. (get-neighbor-distance current-node-name° neighbor-name)]
  110. ;; At first the tentative score is the distance
  111. ;; from the start to the neighbor going via the
  112. ;; current node.
  113. [tentative-score
  114. (+ (hash-table-ref cheapest-path-costs
  115. current-node-name°)
  116. distance)])
  117. ;; If we have found a cheaper path to the neighbor
  118. ;; ...
  119. (when (< tentative-score
  120. (hash-table-ref cheapest-path-costs
  121. neighbor-name))
  122. ;; ... update the preceding node on the path to
  123. ;; the neighbor in the routes map ...
  124. (hash-table-set! routes
  125. neighbor-name
  126. current-node-name°)
  127. ;; ... and update the cheapest path costs for the
  128. ;; neighbor ...
  129. (hash-table-set! cheapest-path-costs
  130. neighbor-name
  131. tentative-score)
  132. ;; ... and update the estimates of the cost of a
  133. ;; path from the start node through the neighbor
  134. ;; to the target node.
  135. (hash-table-set! via-node-cost-estimate
  136. neighbor-name
  137. (+ tentative-score
  138. (cost-heuristic
  139. (hash-table-ref nodes-table current-node-name°)
  140. (hash-table-ref nodes-table neighbor-name)))))))
  141. neighbor-names)
  142. (let ([updated-fringe
  143. (fold (λ (node-name fringe-acc)
  144. ;; Avoid adding visited nodes back to the
  145. ;; fringe, to avoid going in circles.
  146. (if (set-member? visited° node-name)
  147. fringe-acc
  148. (psq-set fringe-acc
  149. node-name
  150. (hash-table-ref cheapest-path-costs
  151. node-name))))
  152. ;; Remove the current node from the fringe.
  153. ;; -- It is now visited and should not be
  154. ;; visited again.
  155. (psq-delete fringe° current-node-name°)
  156. ;; For all neighbors update the
  157. ;; fringe. The fringe is a priority search
  158. ;; queue, which will have the minimum item
  159. ;; readily available at the beginning of
  160. ;; the next iteration.
  161. neighbor-names)])
  162. (cond
  163. [(psq-empty? updated-fringe)
  164. ;; TODO: add proper exception type
  165. (error 'A*
  166. (call-with-output-string
  167. (λ (port)
  168. (simple-format port "no path found from start node to target node")))
  169. (node-name start)
  170. (node-name target))]
  171. [else
  172. (iter (psq-min updated-fringe)
  173. updated-fringe
  174. (set-insert visited° current-node-name°))])))]))))))