/AstarSearching/search.py

https://gitlab.com/iceman201/COSC_Algorithm · Python · 180 lines · 62 code · 37 blank · 81 comment · 17 complexity · 08b5387479d0e89acb849286f6221ecf MD5 · raw file

  1. """This module contains classes and functions related to graph
  2. search. It is specifically written for the course COSC367:
  3. Computational Intelligence. Most of the code here is abstract. The
  4. normal usage would be to write concrete subclasses for particular
  5. problems.
  6. Author: Kourosh Neshatian
  7. Last modified: 20 Jul 2014
  8. """
  9. from abc import ABCMeta, abstractmethod
  10. from collections import namedtuple
  11. def generic_search(graph, frontier):
  12. """Implements a generic graph search algorithm (Figure 3.4 of the
  13. textbook). The actual search behaviour will depend on the type of
  14. the frontier object. If the function halts it returns either a
  15. solution (a path) to a goal node/state (if there exist one) or it
  16. returns None.
  17. """
  18. for node in graph.starting_nodes():
  19. frontier.add((Arc(None, node, "no action", 0),))
  20. solution = None
  21. for path in frontier:
  22. node_to_expand = path[-1].head
  23. if graph.is_goal(node_to_expand):
  24. solution = path
  25. break
  26. for arc in graph.outgoing_arcs(node_to_expand):
  27. frontier.add(path + (arc,))
  28. return solution
  29. class Arc(namedtuple('Arc', 'tail, head, label, cost')):
  30. """Represents an arc in a graph.
  31. Keyword arguments:
  32. tail -- the source node (state)
  33. head -- the destination node (state)
  34. label -- a string describing the action that must be taken in
  35. order to get from the source state to the destination state.
  36. cost -- a number that specifies the cost of the action
  37. """
  38. class Graph(metaclass=ABCMeta):
  39. """This is an abstract class for graphs. It cannot be directly
  40. instantiated. You should define a subclass of this class
  41. (representing a particular problem) and implement the expected
  42. methods."""
  43. @abstractmethod
  44. def is_goal(self, node):
  45. """Returns true if the given node is a goal state."""
  46. @abstractmethod
  47. def starting_nodes():
  48. """Return a sequence of starting nodes. Often there is only one
  49. starting node but even then the function returns a sequence
  50. with one element. It can be implemented as an iterator.
  51. """
  52. @abstractmethod
  53. def outgoing_arcs(self, tail_node):
  54. """Given a node it returns a sequence of arcs (Arc objects)
  55. which correspond to the actions that can be taken in that
  56. state (node)."""
  57. def estimated_cost_to_goal(self, node):
  58. """Return the estimated cost to a goal node from the given
  59. state. This function is usually implemented when there is a
  60. single goal state. The function is used as a heuristic in
  61. search. The implementation should make sure that the heuristic
  62. meets the required criteria for heuristics."""
  63. raise NotImplementedError
  64. class ExplicitGraph(Graph):
  65. """This is a concrete subclass of Graph where vertices and edges
  66. are explicitly enumerated. Objects of this type are useful for
  67. testing graph algorithms."""
  68. def __init__(self, nodes, edge_list, starting_list, goal_nodes, estimates=None):
  69. """Initialises an explicit graph.
  70. Keyword arguments:
  71. nodes -- a set of nodes
  72. edge_list -- a sequence of tuples in the form (tail, head) or
  73. (tail, head, cost)
  74. starting_list -- the list of starting nodes (states)
  75. goal_node -- the set of goal nodes (states)
  76. """
  77. # A few assertions to detect possible errors in
  78. # instantiation. These assertions are not essential to the
  79. # class functionality.
  80. assert all(tail in nodes and head in nodes for tail, head, *_ in edge_list)\
  81. , "edges must link two existing nodes!"
  82. assert all(node in nodes for node in starting_list),\
  83. "The starting_states must be in nodes."
  84. assert all(node in nodes for node in goal_nodes),\
  85. "The goal states must be in nodes."
  86. self.nodes = nodes
  87. self.edge_list = edge_list
  88. self.starting_list = starting_list
  89. self.goal_nodes = goal_nodes
  90. self.estimates = estimates
  91. def starting_nodes(self):
  92. """Returns (via a generator) a sequece of starting nodes."""
  93. for starting_node in self.starting_list:
  94. yield starting_node
  95. def is_goal(self, node):
  96. """Returns true if the given node is a goal node."""
  97. return node in self.goal_nodes
  98. def outgoing_arcs(self, node):
  99. """Returns a sequence of Arc objects corresponding to all the
  100. edges in which the given node is the tail node. The label is
  101. automatically generated."""
  102. for edge in self.edge_list:
  103. tail, head = edge[:2]
  104. if tail == node:
  105. cost = edge[2] if len(edge) > 2 else 1
  106. yield Arc(tail, head, str(tail) + '->' + str(head), cost)
  107. class Frontier(metaclass = ABCMeta):
  108. """This is an abstract class for frontier classes. It outlines the
  109. methods that must be implemented by a concrete subclass. Concrete
  110. subclasses will determine the search strategy.
  111. """
  112. @abstractmethod
  113. def add(self, path):
  114. """Adds a new path to the frontier. A path is a sequence (tuple) of
  115. Arc objects. You should override this method.
  116. """
  117. @abstractmethod
  118. def __iter__(self):
  119. """Returns a generator. The generator selects and removes a path from
  120. the frontier and returns it. A path is a sequence (tuple) of
  121. Arc objects. Override this method according to the desired
  122. search strategy.
  123. """
  124. def print_actions(path):
  125. """Given a path (a sequence of Arc objects), prints the actions
  126. (arc labels) that need to be taken and the total cost of those
  127. actions. The path is usually a solution (a path from the starting
  128. node to a goal node."""
  129. if path:
  130. print("Actions:")
  131. for arc in path[1:-1]:
  132. print(" {},".format(arc.label))
  133. print(" {}.".format(path[-1].label))
  134. print("Total Cost:", sum(arc.cost for arc in path))
  135. else:
  136. print("There is no solution!")