From 0cd061206ede84cf6f6c808e4cd2064f752f7c54 Mon Sep 17 00:00:00 2001 From: Nouman Ahmed <35970677+Noumanmufc1@users.noreply.github.com> Date: Tue, 13 Mar 2018 16:09:40 +0500 Subject: [PATCH 001/200] Added test for SimpleReflexAgentProgram (#808) * Added test for simpleReflexAgent * Fixed a bug * Fixed another bug --- README.md | 2 +- tests/test_agents.py | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a793deb30..968632477 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 2.3 | Table-Driven-Vacuum-Agent | `TableDrivenVacuumAgent` | [`agents.py`][agents] | Done | Included | | 2.7 | Table-Driven-Agent | `TableDrivenAgent` | [`agents.py`][agents] | Done | Included | | 2.8 | Reflex-Vacuum-Agent | `ReflexVacuumAgent` | [`agents.py`][agents] | Done | Included | -| 2.10 | Simple-Reflex-Agent | `SimpleReflexAgent` | [`agents.py`][agents] | | Included | +| 2.10 | Simple-Reflex-Agent | `SimpleReflexAgent` | [`agents.py`][agents] | Done | Included | | 2.12 | Model-Based-Reflex-Agent | `ReflexAgentWithState` | [`agents.py`][agents] | | Included | | 3 | Problem | `Problem` | [`search.py`][search] | Done | Included | | 3 | Node | `Node` | [`search.py`][search] | Done | Included | diff --git a/tests/test_agents.py b/tests/test_agents.py index caefe61d4..d5f63bc48 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -2,7 +2,8 @@ from agents import Direction from agents import Agent from agents import ReflexVacuumAgent, ModelBasedVacuumAgent, TrivialVacuumEnvironment, compare_agents,\ - RandomVacuumAgent, TableDrivenVacuumAgent, TableDrivenAgentProgram, RandomAgentProgram + RandomVacuumAgent, TableDrivenVacuumAgent, TableDrivenAgentProgram, RandomAgentProgram, \ + SimpleReflexAgentProgram, rule_match random.seed("aima-python") @@ -131,6 +132,38 @@ def test_ReflexVacuumAgent() : # check final status of the environment assert environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} +def test_SimpleReflexAgentProgram(): + class Rule: + + def __init__(self, state, action): + self.__state = state + self.action = action + + def matches(self, state): + return self.__state == state + + loc_A = (0, 0) + loc_B = (1, 0) + + # create rules for a two state Vacuum Environment + rules = [Rule((loc_A, "Dirty"), "Suck"), Rule((loc_A, "Clean"), "Right"), + Rule((loc_B, "Dirty"), "Suck"), Rule((loc_B, "Clean"), "Left")] + + def interpret_input(state): + return state + + # create a program and then an object of the SimpleReflexAgentProgram + program = SimpleReflexAgentProgram(rules, interpret_input) + agent = Agent(program) + # create an object of TrivialVacuumEnvironment + environment = TrivialVacuumEnvironment() + # add agent to the environment + environment.add_thing(agent) + # run the environment + environment.run() + # check final status of the environment + assert environment.status == {(1,0):'Clean' , (0,0) : 'Clean'} + def test_ModelBasedVacuumAgent() : # create an object of the ModelBasedVacuumAgent From dc16a97cdc029be0f78cd49944bd6a06ab72c918 Mon Sep 17 00:00:00 2001 From: Aabir Abubaker Kar <16526730+bakerwho@users.noreply.github.com> Date: Tue, 13 Mar 2018 07:10:40 -0400 Subject: [PATCH 002/200] Move viz code + changes to search (#812) * Updating submodule * Moved viz code to notebook.py + changes * Changed use of 'next' * Added networkx to .travis.yml * Added others to .travis.yml * Remove time from .travis.yml * Added linebreaks and fixed case for no algo * Fixed spaces for args * Renamed *search as *search_for_vis --- .travis.yml | 2 + notebook.py | 156 ++++ search.ipynb | 2280 ++++++-------------------------------------------- search.py | 56 +- 4 files changed, 468 insertions(+), 2026 deletions(-) diff --git a/.travis.yml b/.travis.yml index e0932e6b2..600d6bd00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,8 @@ install: - pip install flake8 - pip install ipython - pip install matplotlib + - pip install networkx + - pip install ipywidgets script: - py.test diff --git a/notebook.py b/notebook.py index 6e1a0fbfc..ae0976900 100644 --- a/notebook.py +++ b/notebook.py @@ -886,3 +886,159 @@ def draw_table(self): self.fill(0, 0, 0) self.text_n(self.table[self.context[0]][self.context[1]] if self.context else "Click for text", 0.025, 0.975) self.update() + +############################################################################################################ + +##################### Functions to assist plotting in search.ipynb #################### + +############################################################################################################ +import networkx as nx +import matplotlib.pyplot as plt +from matplotlib import lines + +from ipywidgets import interact +import ipywidgets as widgets +from IPython.display import display +import time +from search import GraphProblem, romania_map + +def show_map(graph_data, node_colors = None): + G = nx.Graph(graph_data['graph_dict']) + node_colors = node_colors or graph_data['node_colors'] + node_positions = graph_data['node_positions'] + node_label_pos = graph_data['node_label_positions'] + edge_weights= graph_data['edge_weights'] + + # set the size of the plot + plt.figure(figsize=(18,13)) + # draw the graph (both nodes and edges) with locations from romania_locations + nx.draw(G, pos = {k : node_positions[k] for k in G.nodes()}, + node_color = [node_colors[node] for node in G.nodes()], linewidths = 0.3, edgecolors = 'k') + + # draw labels for nodes + node_label_handles = nx.draw_networkx_labels(G, pos = node_label_pos, font_size = 14) + + # add a white bounding box behind the node labels + [label.set_bbox(dict(facecolor='white', edgecolor='none')) for label in node_label_handles.values()] + + # add edge lables to the graph + nx.draw_networkx_edge_labels(G, pos = node_positions, edge_labels = edge_weights, font_size = 14) + + # add a legend + white_circle = lines.Line2D([], [], color="white", marker='o', markersize=15, markerfacecolor="white") + orange_circle = lines.Line2D([], [], color="orange", marker='o', markersize=15, markerfacecolor="orange") + red_circle = lines.Line2D([], [], color="red", marker='o', markersize=15, markerfacecolor="red") + gray_circle = lines.Line2D([], [], color="gray", marker='o', markersize=15, markerfacecolor="gray") + green_circle = lines.Line2D([], [], color="green", marker='o', markersize=15, markerfacecolor="green") + plt.legend((white_circle, orange_circle, red_circle, gray_circle, green_circle), + ('Un-explored', 'Frontier', 'Currently Exploring', 'Explored', 'Final Solution'), + numpoints=1,prop={'size':16}, loc=(.8,.75)) + + # show the plot. No need to use in notebooks. nx.draw will show the graph itself. + plt.show() + +## helper functions for visualisations + +def final_path_colors(initial_node_colors, problem, solution): + "returns a node_colors dict of the final path provided the problem and solution" + + # get initial node colors + final_colors = dict(initial_node_colors) + # color all the nodes in solution and starting node to green + final_colors[problem.initial] = "green" + for node in solution: + final_colors[node] = "green" + return final_colors + +def display_visual(graph_data, user_input, algorithm=None, problem=None): + initial_node_colors = graph_data['node_colors'] + if user_input == False: + def slider_callback(iteration): + # don't show graph for the first time running the cell calling this function + try: + show_map(graph_data, node_colors = all_node_colors[iteration]) + except: + pass + def visualize_callback(Visualize): + if Visualize is True: + button.value = False + + global all_node_colors + + iterations, all_node_colors, node = algorithm(problem) + solution = node.solution() + all_node_colors.append(final_path_colors(all_node_colors[0], problem, solution)) + + slider.max = len(all_node_colors) - 1 + + for i in range(slider.max + 1): + slider.value = i + #time.sleep(.5) + + slider = widgets.IntSlider(min=0, max=1, step=1, value=0) + slider_visual = widgets.interactive(slider_callback, iteration = slider) + display(slider_visual) + + button = widgets.ToggleButton(value = False) + button_visual = widgets.interactive(visualize_callback, Visualize = button) + display(button_visual) + + if user_input == True: + node_colors = dict(initial_node_colors) + if isinstance(algorithm, dict): + assert set(algorithm.keys()).issubset(set(["Breadth First Tree Search", + "Depth First Tree Search", + "Breadth First Search", + "Depth First Graph Search", + "Uniform Cost Search", + "A-star Search"])) + + algo_dropdown = widgets.Dropdown(description = "Search algorithm: ", + options = sorted(list(algorithm.keys())), + value = "Breadth First Tree Search") + display(algo_dropdown) + elif algorithm is None: + print("No algorithm to run.") + return 0 + + def slider_callback(iteration): + # don't show graph for the first time running the cell calling this function + try: + show_map(graph_data, node_colors = all_node_colors[iteration]) + except: + pass + + def visualize_callback(Visualize): + if Visualize is True: + button.value = False + + problem = GraphProblem(start_dropdown.value, end_dropdown.value, romania_map) + global all_node_colors + + user_algorithm = algorithm[algo_dropdown.value] + + iterations, all_node_colors, node = user_algorithm(problem) + solution = node.solution() + all_node_colors.append(final_path_colors(all_node_colors[0], problem, solution)) + + slider.max = len(all_node_colors) - 1 + + for i in range(slider.max + 1): + slider.value = i + #time.sleep(.5) + + start_dropdown = widgets.Dropdown(description = "Start city: ", + options = sorted(list(node_colors.keys())), value = "Arad") + display(start_dropdown) + + end_dropdown = widgets.Dropdown(description = "Goal city: ", + options = sorted(list(node_colors.keys())), value = "Fagaras") + display(end_dropdown) + + button = widgets.ToggleButton(value = False) + button_visual = widgets.interactive(visualize_callback, Visualize = button) + display(button_visual) + + slider = widgets.IntSlider(min=0, max=1, step=1, value=0) + slider_visual = widgets.interactive(slider_callback, iteration = slider) + display(slider_visual) \ No newline at end of file diff --git a/search.ipynb b/search.ipynb index edcdf592f..1ac4b075a 100644 --- a/search.ipynb +++ b/search.ipynb @@ -13,14 +13,15 @@ }, { "cell_type": "code", - "execution_count": 134, + "execution_count": null, "metadata": { + "collapsed": true, "scrolled": true }, "outputs": [], "source": [ "from search import *\n", - "from notebook import psource\n", + "from notebook import psource, show_map, final_path_colors, display_visual\n", "\n", "# Needed to hide warnings in the matplotlib sections\n", "import warnings\n", @@ -73,6 +74,32 @@ "*Don't miss the visualisations of these algorithms solving the route-finding problem defined on Romania map at the end of this notebook.*" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For visualisations, we use networkx and matplotlib to show the map in the notebook and we use ipywidgets to interact with the map to see how the searching algorithm works. These are imported as required in `notebook.py`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import networkx as nx\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib import lines\n", + "\n", + "from ipywidgets import interact\n", + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "import time" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -84,159 +111,9 @@ }, { "cell_type": "code", - "execution_count": 135, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
class Problem(object):\n",
-       "\n",
-       "    """The abstract class for a formal problem. You should subclass\n",
-       "    this and implement the methods actions and result, and possibly\n",
-       "    __init__, goal_test, and path_cost. Then you will create instances\n",
-       "    of your subclass and solve them with the various search functions."""\n",
-       "\n",
-       "    def __init__(self, initial, goal=None):\n",
-       "        """The constructor specifies the initial state, and possibly a goal\n",
-       "        state, if there is a unique goal. Your subclass's constructor can add\n",
-       "        other arguments."""\n",
-       "        self.initial = initial\n",
-       "        self.goal = goal\n",
-       "\n",
-       "    def actions(self, state):\n",
-       "        """Return the actions that can be executed in the given\n",
-       "        state. The result would typically be a list, but if there are\n",
-       "        many actions, consider yielding them one at a time in an\n",
-       "        iterator, rather than building them all at once."""\n",
-       "        raise NotImplementedError\n",
-       "\n",
-       "    def result(self, state, action):\n",
-       "        """Return the state that results from executing the given\n",
-       "        action in the given state. The action must be one of\n",
-       "        self.actions(state)."""\n",
-       "        raise NotImplementedError\n",
-       "\n",
-       "    def goal_test(self, state):\n",
-       "        """Return True if the state is a goal. The default method compares the\n",
-       "        state to self.goal or checks for state in self.goal if it is a\n",
-       "        list, as specified in the constructor. Override this method if\n",
-       "        checking against a single self.goal is not enough."""\n",
-       "        if isinstance(self.goal, list):\n",
-       "            return is_in(state, self.goal)\n",
-       "        else:\n",
-       "            return state == self.goal\n",
-       "\n",
-       "    def path_cost(self, c, state1, action, state2):\n",
-       "        """Return the cost of a solution path that arrives at state2 from\n",
-       "        state1 via action, assuming cost c to get up to state1. If the problem\n",
-       "        is such that the path doesn't matter, this function will only look at\n",
-       "        state2.  If the path does matter, it will consider c and maybe state1\n",
-       "        and action. The default method costs 1 for every step in the path."""\n",
-       "        return c + 1\n",
-       "\n",
-       "    def value(self, state):\n",
-       "        """For optimization problems, each state has a value.  Hill-climbing\n",
-       "        and related algorithms try to maximize this value."""\n",
-       "        raise NotImplementedError\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "psource(Problem)" ] @@ -276,171 +153,9 @@ }, { "cell_type": "code", - "execution_count": 136, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
class Node:\n",
-       "\n",
-       "    """A node in a search tree. Contains a pointer to the parent (the node\n",
-       "    that this is a successor of) and to the actual state for this node. Note\n",
-       "    that if a state is arrived at by two paths, then there are two nodes with\n",
-       "    the same state.  Also includes the action that got us to this state, and\n",
-       "    the total path_cost (also known as g) to reach the node.  Other functions\n",
-       "    may add an f and h value; see best_first_graph_search and astar_search for\n",
-       "    an explanation of how the f and h values are handled. You will not need to\n",
-       "    subclass this class."""\n",
-       "\n",
-       "    def __init__(self, state, parent=None, action=None, path_cost=0):\n",
-       "        """Create a search tree Node, derived from a parent by an action."""\n",
-       "        self.state = state\n",
-       "        self.parent = parent\n",
-       "        self.action = action\n",
-       "        self.path_cost = path_cost\n",
-       "        self.depth = 0\n",
-       "        if parent:\n",
-       "            self.depth = parent.depth + 1\n",
-       "\n",
-       "    def __repr__(self):\n",
-       "        return "<Node {}>".format(self.state)\n",
-       "\n",
-       "    def __lt__(self, node):\n",
-       "        return self.state < node.state\n",
-       "\n",
-       "    def expand(self, problem):\n",
-       "        """List the nodes reachable in one step from this node."""\n",
-       "        return [self.child_node(problem, action)\n",
-       "                for action in problem.actions(self.state)]\n",
-       "\n",
-       "    def child_node(self, problem, action):\n",
-       "        """[Figure 3.10]"""\n",
-       "        next = problem.result(self.state, action)\n",
-       "        return Node(next, self, action,\n",
-       "                    problem.path_cost(self.path_cost, self.state,\n",
-       "                                      action, next))\n",
-       "\n",
-       "    def solution(self):\n",
-       "        """Return the sequence of actions to go from the root to this node."""\n",
-       "        return [node.action for node in self.path()[1:]]\n",
-       "\n",
-       "    def path(self):\n",
-       "        """Return a list of nodes forming the path from the root to this node."""\n",
-       "        node, path_back = self, []\n",
-       "        while node:\n",
-       "            path_back.append(node)\n",
-       "            node = node.parent\n",
-       "        return list(reversed(path_back))\n",
-       "\n",
-       "    # We want for a queue of nodes in breadth_first_search or\n",
-       "    # astar_search to have no duplicated states, so we treat nodes\n",
-       "    # with the same state as equal. [Problem: this may not be what you\n",
-       "    # want in other contexts.]\n",
-       "\n",
-       "    def __eq__(self, other):\n",
-       "        return isinstance(other, Node) and self.state == other.state\n",
-       "\n",
-       "    def __hash__(self):\n",
-       "        return hash(self.state)\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "psource(Node)" ] @@ -479,148 +194,9 @@ }, { "cell_type": "code", - "execution_count": 137, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
class GraphProblem(Problem):\n",
-       "\n",
-       "    """The problem of searching a graph from one node to another."""\n",
-       "\n",
-       "    def __init__(self, initial, goal, graph):\n",
-       "        Problem.__init__(self, initial, goal)\n",
-       "        self.graph = graph\n",
-       "\n",
-       "    def actions(self, A):\n",
-       "        """The actions at a graph node are just its neighbors."""\n",
-       "        return list(self.graph.get(A).keys())\n",
-       "\n",
-       "    def result(self, state, action):\n",
-       "        """The result of going to a neighbor is just that neighbor."""\n",
-       "        return action\n",
-       "\n",
-       "    def path_cost(self, cost_so_far, A, action, B):\n",
-       "        return cost_so_far + (self.graph.get(A, B) or infinity)\n",
-       "\n",
-       "    def find_min_edge(self):\n",
-       "        """Find minimum value of edges."""\n",
-       "        m = infinity\n",
-       "        for d in self.graph.dict.values():\n",
-       "            local_min = min(d.values())\n",
-       "            m = min(m, local_min)\n",
-       "\n",
-       "        return m\n",
-       "\n",
-       "    def h(self, node):\n",
-       "        """h function is straight-line distance from a node's state to goal."""\n",
-       "        locs = getattr(self.graph, 'locations', None)\n",
-       "        if locs:\n",
-       "            if type(node) is str:\n",
-       "                return int(distance(locs[node], locs[self.goal]))\n",
-       "\n",
-       "            return int(distance(locs[node.state], locs[self.goal]))\n",
-       "        else:\n",
-       "            return infinity\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "psource(GraphProblem)" ] @@ -634,8 +210,10 @@ }, { "cell_type": "code", - "execution_count": 138, - "metadata": {}, + "execution_count": null, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "romania_map = UndirectedGraph(dict(\n", @@ -679,8 +257,10 @@ }, { "cell_type": "code", - "execution_count": 139, - "metadata": {}, + "execution_count": null, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)" @@ -704,46 +284,14 @@ }, { "cell_type": "code", - "execution_count": 140, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'Arad': (91, 492), 'Bucharest': (400, 327), 'Craiova': (253, 288), 'Drobeta': (165, 299), 'Eforie': (562, 293), 'Fagaras': (305, 449), 'Giurgiu': (375, 270), 'Hirsova': (534, 350), 'Iasi': (473, 506), 'Lugoj': (165, 379), 'Mehadia': (168, 339), 'Neamt': (406, 537), 'Oradea': (131, 571), 'Pitesti': (320, 368), 'Rimnicu': (233, 410), 'Sibiu': (207, 457), 'Timisoara': (94, 410), 'Urziceni': (456, 350), 'Vaslui': (509, 444), 'Zerind': (108, 531)}\n" - ] - } - ], + "outputs": [], "source": [ "romania_locations = romania_map.locations\n", "print(romania_locations)" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's start the visualisations by importing necessary modules. We use networkx and matplotlib to show the map in the notebook and we use ipywidgets to interact with the map to see how the searching algorithm works." - ] - }, - { - "cell_type": "code", - "execution_count": 141, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "import networkx as nx\n", - "import matplotlib.pyplot as plt\n", - "from matplotlib import lines\n", - "\n", - "from ipywidgets import interact\n", - "import ipywidgets as widgets\n", - "from IPython.display import display\n", - "import time" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -753,46 +301,24 @@ }, { "cell_type": "code", - "execution_count": 142, - "metadata": {}, + "execution_count": null, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ - "# initialise a graph\n", - "G = nx.Graph()\n", - "\n", - "# use this while labeling nodes in the map\n", - "node_labels = dict()\n", - "# use this to modify colors of nodes while exploring the graph.\n", - "# This is the only dict we send to `show_map(node_colors)` while drawing the map\n", - "node_colors = dict()\n", - "\n", - "for n, p in romania_locations.items():\n", - " # add nodes from romania_locations\n", - " G.add_node(n)\n", - " # add nodes to node_labels\n", - " node_labels[n] = n\n", - " # node_colors to color nodes while exploring romania map\n", - " node_colors[n] = \"white\"\n", + "# node colors, node positions and node label positions\n", + "node_colors = {node: 'white' for node in romania_map.locations.keys()}\n", + "node_positions = romania_map.locations\n", + "node_label_pos = { k:[v[0],v[1]-10] for k,v in romania_map.locations.items() }\n", + "edge_weights = {(k, k2) : v2 for k, v in romania_map.graph_dict.items() for k2, v2 in v.items()}\n", "\n", - "# we'll save the initial node colors to a dict to use later\n", - "initial_node_colors = dict(node_colors)\n", - " \n", - "# positions for node labels\n", - "node_label_pos = { k:[v[0],v[1]-10] for k,v in romania_locations.items() }\n", - "\n", - "# use this while labeling edges\n", - "edge_labels = dict()\n", - "\n", - "# add edges between cities in romania map - UndirectedGraph defined in search.py\n", - "for node in romania_map.nodes():\n", - " connections = romania_map.get(node)\n", - " for connection in connections.keys():\n", - " distance = connections[connection]\n", - "\n", - " # add edges to the graph\n", - " G.add_edge(node, connection)\n", - " # add distances to edge_labels\n", - " edge_labels[(node, connection)] = distance" + "romania_graph_data = { 'graph_dict' : romania_map.graph_dict,\n", + " 'node_colors': node_colors,\n", + " 'node_positions': node_positions,\n", + " 'node_label_positions': node_label_pos,\n", + " 'edge_weights': edge_weights\n", + " }" ] }, { @@ -802,40 +328,6 @@ "We have completed building our graph based on romania_map and its locations. It's time to display it here in the notebook. This function `show_map(node_colors)` helps us do that. We will be calling this function later on to display the map at each and every interval step while searching, using variety of algorithms from the book." ] }, - { - "cell_type": "code", - "execution_count": 143, - "metadata": {}, - "outputs": [], - "source": [ - "def show_map(node_colors):\n", - " # set the size of the plot\n", - " plt.figure(figsize=(18,13))\n", - " # draw the graph (both nodes and edges) with locations from romania_locations\n", - " nx.draw(G, pos = romania_locations, node_color = [node_colors[node] for node in G.nodes()])\n", - "\n", - " # draw labels for nodes\n", - " node_label_handles = nx.draw_networkx_labels(G, pos = node_label_pos, labels = node_labels, font_size = 14)\n", - " # add a white bounding box behind the node labels\n", - " [label.set_bbox(dict(facecolor='white', edgecolor='none')) for label in node_label_handles.values()]\n", - "\n", - " # add edge lables to the graph\n", - " nx.draw_networkx_edge_labels(G, pos = romania_locations, edge_labels=edge_labels, font_size = 14)\n", - " \n", - " # add a legend\n", - " white_circle = lines.Line2D([], [], color=\"white\", marker='o', markersize=15, markerfacecolor=\"white\")\n", - " orange_circle = lines.Line2D([], [], color=\"orange\", marker='o', markersize=15, markerfacecolor=\"orange\")\n", - " red_circle = lines.Line2D([], [], color=\"red\", marker='o', markersize=15, markerfacecolor=\"red\")\n", - " gray_circle = lines.Line2D([], [], color=\"gray\", marker='o', markersize=15, markerfacecolor=\"gray\")\n", - " green_circle = lines.Line2D([], [], color=\"green\", marker='o', markersize=15, markerfacecolor=\"green\")\n", - " plt.legend((white_circle, orange_circle, red_circle, gray_circle, green_circle),\n", - " ('Un-explored', 'Frontier', 'Currently Exploring', 'Explored', 'Final Solution'),\n", - " numpoints=1,prop={'size':16}, loc=(.8,.75))\n", - " \n", - " # show the plot. No need to use in notebooks. nx.draw will show the graph itself.\n", - " plt.show()" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -845,24 +337,13 @@ }, { "cell_type": "code", - "execution_count": 144, + "execution_count": null, "metadata": { "scrolled": true }, - "outputs": [ - { - "data": { - "image/png": "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Ll68qNmzZysoKChTeZmYmJixVP3mkvf4+Hi5urpmmSV685rExESVKVNGBQrwz0tOud+ZnQkJCerXr5+WL1+usmXL5mAyAMhb0tPTtXDhQn333XdmRwEAALmIT6MA4IROnDihdu3a6fr164qLi5Ovr6/mzJmj1q1bq0mTJoqIiNDUqVPVokULvf/++ypWrJikv/fvNAxDxYoV09WrV1W0aFFJkqurqyRp3759cnd3zzit/d+zOoOCgnT48GFVqVIly8nxVqtVPj4+KliwYO79IJyQv7+/4uLilJaWdtdSOT09Xb1799aQIUPUunXrXEoIAHnDhg0b5OXlpdq1a5sdBQAA5CJOYwcAJ2QYhvbv3689e/YoISFBu3bt0q5du9SgQQPNmDFDdevW1fnz5xUUFKRGjRqpWrVqCggIUO3ateXm5iYXFxf16dNHR48e1YoVK1SpUiVJUsOGDdWgQQNNnTo1oyD9t5SUFP31118ZJ8b/8/T4EydOyMvLK0sJarVa5evryzLDe1S1alVt3rxZ/v7+d7wuLCxMP/30k77//vuMwhoA8ovnnntO7du310svvWR2FAAAkIsoOwEgHzp8+LCioqK0detW7d+/X7GxsTp69KimT5+uQYMGycXFRXv27FGvXr3UsWNHdejQQZ9++qk2btyoH3/8UXXr1n2gcVNTU3X06NEsJWh0dLTi4+NVoUKFWxahfn5+cnd3t/NPwXE98cQTeuONNxQUFHTba3788Uf16tVLu3fvVsWKFXMxHQCYLzExUdWqVVN8fPxt/zgHAACcE2UnACCDzWbLdIDNV199pUmTJik2NlaNGzfW+PHj1ahRoxwZOy0tTfHx8VlK0OjoaP31118qW7ZslhLUarXK399fHh4eOZIprxoyZIiqV6+u4cOH3/L506dPq0GDBlqwYIHatWuXy+kAwHxTpkzRH3/8oYULF5odBQAA5DLKTgDZFhwcrLNnz2rt2rVmR0EOMvPk9fT0dB07dixLCRodHa3Y2FiVKFEiSwl688vT09OUzDkpOjpaRYoUydhe4J9sNps6duyoevXq6f333zchHQCYyzAM1ahRQ/PmzdMjjzxidhwAAJDLOKAIyAeCg4P12WefSZIKFCigkiVLqmbNmnr++ef10ksv5YkDY24eorNz584cmzmI7DGr6JT+PiDJx8dHPj4+atu2babnbDab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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "show_map(node_colors)" + "show_map(romania_graph_data)" ] }, { @@ -883,144 +364,9 @@ }, { "cell_type": "code", - "execution_count": 145, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
class SimpleProblemSolvingAgentProgram:\n",
-       "\n",
-       "    """Abstract framework for a problem-solving agent. [Figure 3.1]"""\n",
-       "\n",
-       "    def __init__(self, initial_state=None):\n",
-       "        """State is an abstract representation of the state\n",
-       "        of the world, and seq is the list of actions required\n",
-       "        to get to a particular state from the initial state(root)."""\n",
-       "        self.state = initial_state\n",
-       "        self.seq = []\n",
-       "\n",
-       "    def __call__(self, percept):\n",
-       "        """[Figure 3.1] Formulate a goal and problem, then\n",
-       "        search for a sequence of actions to solve it."""\n",
-       "        self.state = self.update_state(self.state, percept)\n",
-       "        if not self.seq:\n",
-       "            goal = self.formulate_goal(self.state)\n",
-       "            problem = self.formulate_problem(self.state, goal)\n",
-       "            self.seq = self.search(problem)\n",
-       "            if not self.seq:\n",
-       "                return None\n",
-       "        return self.seq.pop(0)\n",
-       "\n",
-       "    def update_state(self, percept):\n",
-       "        raise NotImplementedError\n",
-       "\n",
-       "    def formulate_goal(self, state):\n",
-       "        raise NotImplementedError\n",
-       "\n",
-       "    def formulate_problem(self, state, goal):\n",
-       "        raise NotImplementedError\n",
-       "\n",
-       "    def search(self, problem):\n",
-       "        raise NotImplementedError\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "psource(SimpleProblemSolvingAgentProgram)" ] @@ -1055,8 +401,10 @@ }, { "cell_type": "code", - "execution_count": 146, - "metadata": {}, + "execution_count": null, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "class vacuumAgent(SimpleProblemSolvingAgentProgram):\n", @@ -1096,34 +444,24 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Left\n", - "Suck\n", - "Right\n" - ] - } - ], + "outputs": [], "source": [ - " state1 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", - " state2 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", - " state3 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", - " state4 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", - " state5 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", - " state6 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", - " state7 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", - " state8 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", + "state1 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", + "state2 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Dirty\"]]]\n", + "state3 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", + "state4 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Dirty\"]]]\n", + "state5 = [(0, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", + "state6 = [(1, 0), [(0, 0), \"Dirty\"], [(1, 0), [\"Clean\"]]]\n", + "state7 = [(0, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", + "state8 = [(1, 0), [(0, 0), \"Clean\"], [(1, 0), [\"Clean\"]]]\n", "\n", - " a = vacuumAgent(state1)\n", + "a = vacuumAgent(state1)\n", "\n", - " print(a(state6)) \n", - " print(a(state1))\n", - " print(a(state3))" + "print(a(state6)) \n", + "print(a(state1))\n", + "print(a(state3))" ] }, { @@ -1134,157 +472,42 @@ "\n", "In this section, we have visualizations of the following searching algorithms:\n", "\n", - "1. Breadth First Tree Search - Implemented\n", - "2. Depth First Tree Search - Implemented\n", - "3. Depth First Graph Search - Implemented\n", - "4. Breadth First Search - Implemented\n", - "5. Best First Graph Search - Implemented\n", - "6. Uniform Cost Search - Implemented\n", + "1. Breadth First Tree Search\n", + "2. Depth First Tree Search\n", + "3. Breadth First Search\n", + "4. Depth First Graph Search\n", + "5. Best First Graph Search\n", + "6. Uniform Cost Search\n", "7. Depth Limited Search\n", "8. Iterative Deepening Search\n", - "9. A\\*-Search - Implemented\n", + "9. A\\*-Search\n", "10. Recursive Best First Search\n", "\n", "We add the colors to the nodes to have a nice visualisation when displaying. So, these are the different colors we are using in these visuals:\n", "* Un-explored nodes - white\n", "* Frontier nodes - orange\n", "* Currently exploring node - red\n", - "* Already explored nodes - gray\n", - "\n", - "Now, we will define some helper methods to display interactive buttons and sliders when visualising search algorithms." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "def final_path_colors(problem, solution):\n", - " \"returns a node_colors dict of the final path provided the problem and solution\"\n", - " \n", - " # get initial node colors\n", - " final_colors = dict(initial_node_colors)\n", - " # color all the nodes in solution and starting node to green\n", - " final_colors[problem.initial] = \"green\"\n", - " for node in solution:\n", - " final_colors[node] = \"green\" \n", - " return final_colors\n", - "\n", - "\n", - "def display_visual(user_input, algorithm=None, problem=None):\n", - " if user_input == False:\n", - " def slider_callback(iteration):\n", - " # don't show graph for the first time running the cell calling this function\n", - " try:\n", - " show_map(all_node_colors[iteration])\n", - " except:\n", - " pass\n", - " def visualize_callback(Visualize):\n", - " if Visualize is True:\n", - " button.value = False\n", - " \n", - " global all_node_colors\n", - " \n", - " iterations, all_node_colors, node = algorithm(problem)\n", - " solution = node.solution()\n", - " all_node_colors.append(final_path_colors(problem, solution))\n", - " \n", - " slider.max = len(all_node_colors) - 1\n", - " \n", - " for i in range(slider.max + 1):\n", - " slider.value = i\n", - " #time.sleep(.5)\n", - " \n", - " slider = widgets.IntSlider(min=0, max=1, step=1, value=0)\n", - " slider_visual = widgets.interactive(slider_callback, iteration = slider)\n", - " display(slider_visual)\n", - "\n", - " button = widgets.ToggleButton(value = False)\n", - " button_visual = widgets.interactive(visualize_callback, Visualize = button)\n", - " display(button_visual)\n", - " \n", - " if user_input == True:\n", - " node_colors = dict(initial_node_colors)\n", - " if algorithm == None:\n", - " algorithms = {\"Breadth First Tree Search\": breadth_first_tree_search,\n", - " \"Depth First Tree Search\": depth_first_tree_search,\n", - " \"Breadth First Search\": breadth_first_search,\n", - " \"Depth First Graph Search\": depth_first_graph_search,\n", - " \"Uniform Cost Search\": uniform_cost_search,\n", - " \"A-star Search\": astar_search}\n", - " algo_dropdown = widgets.Dropdown(description = \"Search algorithm: \",\n", - " options = sorted(list(algorithms.keys())),\n", - " value = \"Breadth First Tree Search\")\n", - " display(algo_dropdown)\n", - " \n", - " def slider_callback(iteration):\n", - " # don't show graph for the first time running the cell calling this function\n", - " try:\n", - " show_map(all_node_colors[iteration])\n", - " except:\n", - " pass\n", - " \n", - " def visualize_callback(Visualize):\n", - " if Visualize is True:\n", - " button.value = False\n", - " \n", - " problem = GraphProblem(start_dropdown.value, end_dropdown.value, romania_map)\n", - " global all_node_colors\n", - " \n", - " if algorithm == None:\n", - " user_algorithm = algorithms[algo_dropdown.value]\n", - " \n", - "# print(user_algorithm)\n", - "# print(problem)\n", - " \n", - " iterations, all_node_colors, node = user_algorithm(problem)\n", - " solution = node.solution()\n", - " all_node_colors.append(final_path_colors(problem, solution))\n", - "\n", - " slider.max = len(all_node_colors) - 1\n", - " \n", - " for i in range(slider.max + 1):\n", - " slider.value = i\n", - "# time.sleep(.5)\n", - " \n", - " start_dropdown = widgets.Dropdown(description = \"Start city: \",\n", - " options = sorted(list(node_colors.keys())), value = \"Arad\")\n", - " display(start_dropdown)\n", - "\n", - " end_dropdown = widgets.Dropdown(description = \"Goal city: \",\n", - " options = sorted(list(node_colors.keys())), value = \"Fagaras\")\n", - " display(end_dropdown)\n", - " \n", - " button = widgets.ToggleButton(value = False)\n", - " button_visual = widgets.interactive(visualize_callback, Visualize = button)\n", - " display(button_visual)\n", - " \n", - " slider = widgets.IntSlider(min=0, max=1, step=1, value=0)\n", - " slider_visual = widgets.interactive(slider_callback, iteration = slider)\n", - " display(slider_visual)" + "* Already explored nodes - gray" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## BREADTH-FIRST TREE SEARCH\n", + "## 1. BREADTH-FIRST TREE SEARCH\n", "\n", "We have a working implementation in search module. But as we want to interact with the graph while it is searching, we need to modify the implementation. Here's the modified breadth first tree search." ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ - "def tree_search(problem, frontier):\n", + "def tree_search_for_vis(problem, frontier):\n", " \"\"\"Search through the successors of a problem to find a goal.\n", " The argument frontier should be an empty queue.\n", " Don't worry about repeated paths to a state. [Figure 3.7]\"\"\"\n", @@ -1292,7 +515,7 @@ " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", - " node_colors = dict(initial_node_colors)\n", + " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", " \n", " #Adding first node to the queue\n", " frontier.append(Node(problem.initial))\n", @@ -1333,7 +556,7 @@ "\n", "def breadth_first_tree_search(problem):\n", " \"Search the shallowest nodes in the search tree first.\"\n", - " iterations, all_node_colors, node = tree_search(problem, FIFOQueue())\n", + " iterations, all_node_colors, node = tree_search_for_vis(problem, FIFOQueue())\n", " return(iterations, all_node_colors, node)" ] }, @@ -1346,45 +569,29 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d55324f7343a4c71a9a2d4da6d037037" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b07a3813dd724c51a9b37f646cf2be25" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Fagaras', romania_map)\n", - "display_visual(user_input = False, algorithm = breadth_first_tree_search, problem = romania_problem)" + "a, b, c = breadth_first_tree_search(romania_problem)\n", + "display_visual(romania_graph_data, user_input=False, \n", + " algorithm=breadth_first_tree_search, \n", + " problem=romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Depth-First Tree Search:\n", + "## 2. Depth-First Tree Search:\n", "Now let's discuss another searching algorithm, Depth-First Tree Search." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": { "collapsed": true }, @@ -1394,38 +601,21 @@ " \"Search the deepest nodes in the search tree first.\"\n", " # This algorithm might not work in case of repeated paths\n", " # and may run into an infinite while loop.\n", - " iterations, all_node_colors, node = tree_search(problem, Stack())\n", + " iterations, all_node_colors, node = tree_search_for_vis(problem, Stack())\n", " return(iterations, all_node_colors, node)" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "523b10cf84e54798a044ee714b864b52" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "aecea953f6a448c192ac8e173cf46e35" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Oradea', romania_map)\n", - "display_visual(user_input = False, algorithm = depth_first_tree_search, problem = romania_problem)" + "display_visual(romania_graph_data, user_input=False, \n", + " algorithm=depth_first_tree_search, \n", + " problem=romania_problem)" ] }, { @@ -1434,14 +624,14 @@ "collapsed": true }, "source": [ - "## BREADTH-FIRST SEARCH\n", + "## 3. BREADTH-FIRST GRAPH SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": { "collapsed": true }, @@ -1453,7 +643,7 @@ " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", - " node_colors = dict(initial_node_colors)\n", + " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", " \n", " node = Node(problem.initial)\n", " \n", @@ -1505,58 +695,41 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "735a3dea191a42b6bd97fdfd337ea3e7" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ef445770d70a4b7c9d1544b98a55ca4d" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", - "display_visual(user_input = False, algorithm = breadth_first_search, problem = romania_problem)" + "display_visual(romania_graph_data, user_input=False, \n", + " algorithm=breadth_first_search, \n", + " problem=romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Depth-First Graph Search: \n", + "## 4. Depth-First Graph Search: \n", "Although we have a working implementation in search module, we have to make a few changes in the algorithm to make it suitable for visualization." ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ - "def graph_search(problem, frontier):\n", + "def graph_search_for_vis(problem, frontier):\n", " \"\"\"Search through the successors of a problem to find a goal.\n", " The argument frontier should be an empty queue.\n", " If two paths reach a state, only use the first one. [Figure 3.7]\"\"\"\n", " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", - " node_colors = dict(initial_node_colors)\n", + " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", " \n", " frontier.append(Node(problem.initial))\n", " explored = set()\n", @@ -1603,58 +776,41 @@ "\n", "def depth_first_graph_search(problem):\n", " \"\"\"Search the deepest nodes in the search tree first.\"\"\"\n", - " iterations, all_node_colors, node = graph_search(problem, Stack())\n", + " iterations, all_node_colors, node = graph_search_for_vis(problem, Stack())\n", " return(iterations, all_node_colors, node)" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "61149ffbc02846af97170f8975d4f11d" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "90b1f8f77fdb4207a3570fbe88a0bdf6" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", - "display_visual(user_input = False, algorithm = depth_first_graph_search, problem = romania_problem)" + "display_visual(romania_graph_data, user_input=False, \n", + " algorithm=depth_first_graph_search, \n", + " problem=romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## BEST FIRST SEARCH\n", + "## 5. BEST FIRST SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ - "def best_first_graph_search(problem, f):\n", + "def best_first_graph_search_for_vis(problem, f):\n", " \"\"\"Search the nodes with the lowest f scores first.\n", " You specify the function f(node) that you want to minimize; for example,\n", " if f is a heuristic estimate to the goal, then we have greedy best\n", @@ -1666,7 +822,7 @@ " # we use these two variables at the time of visualisations\n", " iterations = 0\n", " all_node_colors = []\n", - " node_colors = dict(initial_node_colors)\n", + " node_colors = {k : 'white' for k in problem.graph.nodes()}\n", " \n", " f = memoize(f, 'f')\n", " node = Node(problem.initial)\n", @@ -1728,14 +884,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## UNIFORM COST SEARCH\n", + "## 6. UNIFORM COST SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": { "collapsed": true }, @@ -1744,38 +900,21 @@ "def uniform_cost_search(problem):\n", " \"[Figure 3.14]\"\n", " #Uniform Cost Search uses Best First Search algorithm with f(n) = g(n)\n", - " iterations, all_node_colors, node = best_first_graph_search(problem, lambda node: node.path_cost)\n", - " return(iterations, all_node_colors, node)" + " iterations, all_node_colors, node = best_first_graph_search_for_vis(problem, lambda node: node.path_cost)\n", + " return(iterations, all_node_colors, node)\n" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "46b8200b4a8f47e7b18145234a8469da" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ca9b2d01bbd5458bb037585c719d73fc" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", - "display_visual(user_input = False, algorithm = uniform_cost_search, problem = romania_problem)" + "display_visual(romania_graph_data, user_input=False, \n", + " algorithm=uniform_cost_search, \n", + " problem=romania_problem)" ] }, { @@ -1788,7 +927,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": { "collapsed": true }, @@ -1799,52 +938,35 @@ " You need to specify the h function when you call best_first_search, or\n", " else in your Problem subclass.\"\"\"\n", " h = memoize(h or problem.h, 'h')\n", - " iterations, all_node_colors, node = best_first_graph_search(problem, lambda n: h(n))\n", - " return(iterations, all_node_colors, node)" + " iterations, all_node_colors, node = best_first_graph_search_for_vis(problem, lambda n: h(n))\n", + " return(iterations, all_node_colors, node)\n" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e3ddd0260d7d4a8aa62d610976b9568a" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "dae485b1f4224c34a88de42d252da76c" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", - "display_visual(user_input = False, algorithm = greedy_best_first_search, problem = romania_problem)" + "display_visual(romania_graph_data, user_input=False, \n", + " algorithm=greedy_best_first_search, \n", + " problem=romania_problem)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## A\\* SEARCH\n", + "## 9. A\\* SEARCH\n", "\n", "Let's change all the `node_colors` to starting position and define a different problem statement." ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": { "collapsed": true }, @@ -1855,97 +977,41 @@ " You need to specify the h function when you call astar_search, or\n", " else in your Problem subclass.\"\"\"\n", " h = memoize(h or problem.h, 'h')\n", - " iterations, all_node_colors, node = best_first_graph_search(problem, lambda n: n.path_cost + h(n))\n", - " return(iterations, all_node_colors, node)" + " iterations, all_node_colors, node = best_first_graph_search_for_vis(problem, \n", + " lambda n: n.path_cost + h(n))\n", + " return(iterations, all_node_colors, node)\n" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "15a78d815f0c4ea589cdd5ad40bc8794" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "10450687dd574be2a380e9e40403fa83" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", - "display_visual(user_input = False, algorithm = astar_search, problem = romania_problem)" + "display_visual(romania_graph_data, user_input=False, \n", + " algorithm=astar_search, \n", + " problem=romania_problem)" ] }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": { "scrolled": false }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9019790cf8324d73966373bb3f5373a8" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b8a3195598da472d996e4e8b81595cb7" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "aabe167a0d6440f0a020df8a85a9206c" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "25d146d187004f4f9db6a7dccdbc7e93" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "68d532810a9e46309415fd353c474a4d" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "all_node_colors = []\n", - "# display_visual(user_input = True, algorithm = breadth_first_tree_search)\n", - "display_visual(user_input = True)" + "# display_visual(romania_graph_data, user_input=True, algorithm=breadth_first_tree_search)\n", + "algorithms = { \"Breadth First Tree Search\": breadth_first_tree_search,\n", + " \"Depth First Tree Search\": depth_first_tree_search,\n", + " \"Breadth First Search\": breadth_first_search,\n", + " \"Depth First Graph Search\": depth_first_graph_search,\n", + " \"Uniform Cost Search\": uniform_cost_search,\n", + " \"A-star Search\": astar_search}\n", + "display_visual(romania_graph_data, algorithm=algorithms, user_input=True)" ] }, { @@ -1982,7 +1048,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": { "collapsed": true }, @@ -2035,57 +1101,9 @@ }, { "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "True\n", - "Number of explored nodes by the following heuristic are: 145\n", - "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", - "[2, 4, 3, 1, 5, 6, 7, 0, 8]\n", - "[2, 4, 3, 1, 0, 6, 7, 5, 8]\n", - "[2, 0, 3, 1, 4, 6, 7, 5, 8]\n", - "[0, 2, 3, 1, 4, 6, 7, 5, 8]\n", - "[1, 2, 3, 0, 4, 6, 7, 5, 8]\n", - "[1, 2, 3, 4, 0, 6, 7, 5, 8]\n", - "[1, 2, 3, 4, 5, 6, 7, 0, 8]\n", - "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n", - "Number of explored nodes by the following heuristic are: 153\n", - "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", - "[2, 4, 3, 1, 5, 6, 7, 0, 8]\n", - "[2, 4, 3, 1, 0, 6, 7, 5, 8]\n", - "[2, 0, 3, 1, 4, 6, 7, 5, 8]\n", - "[0, 2, 3, 1, 4, 6, 7, 5, 8]\n", - "[1, 2, 3, 0, 4, 6, 7, 5, 8]\n", - "[1, 2, 3, 4, 0, 6, 7, 5, 8]\n", - "[1, 2, 3, 4, 5, 6, 7, 0, 8]\n", - "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n", - "Number of explored nodes by the following heuristic are: 145\n", - "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", - "[2, 4, 3, 1, 5, 6, 7, 0, 8]\n", - "[2, 4, 3, 1, 0, 6, 7, 5, 8]\n", - "[2, 0, 3, 1, 4, 6, 7, 5, 8]\n", - "[0, 2, 3, 1, 4, 6, 7, 5, 8]\n", - "[1, 2, 3, 0, 4, 6, 7, 5, 8]\n", - "[1, 2, 3, 4, 0, 6, 7, 5, 8]\n", - "[1, 2, 3, 4, 5, 6, 7, 0, 8]\n", - "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n", - "Number of explored nodes by the following heuristic are: 169\n", - "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", - "[2, 4, 3, 1, 5, 6, 7, 0, 8]\n", - "[2, 4, 3, 1, 0, 6, 7, 5, 8]\n", - "[2, 0, 3, 1, 4, 6, 7, 5, 8]\n", - "[0, 2, 3, 1, 4, 6, 7, 5, 8]\n", - "[1, 2, 3, 0, 4, 6, 7, 5, 8]\n", - "[1, 2, 3, 4, 0, 6, 7, 5, 8]\n", - "[1, 2, 3, 4, 5, 6, 7, 0, 8]\n", - "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "# Solving the puzzle \n", "puzzle = EightPuzzle()\n", @@ -2117,124 +1135,11 @@ }, { "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
def hill_climbing(problem):\n",
-       "    """From the initial node, keep choosing the neighbor with highest value,\n",
-       "    stopping when no neighbor is better. [Figure 4.2]"""\n",
-       "    current = Node(problem.initial)\n",
-       "    while True:\n",
-       "        neighbors = current.expand(problem)\n",
-       "        if not neighbors:\n",
-       "            break\n",
-       "        neighbor = argmax_random_tie(neighbors,\n",
-       "                                     key=lambda node: problem.value(node.state))\n",
-       "        if problem.value(neighbor.state) <= problem.value(current.state):\n",
-       "            break\n",
-       "        current = neighbor\n",
-       "    return current.state\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "psource(hill_climbing)" ] @@ -2252,7 +1157,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": { "collapsed": true }, @@ -2304,17 +1209,11 @@ }, { "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['Arad', 'Bucharest', 'Craiova', 'Drobeta', 'Eforie', 'Fagaras', 'Giurgiu', 'Hirsova', 'Iasi', 'Lugoj', 'Mehadia', 'Neamt', 'Oradea', 'Pitesti', 'Rimnicu', 'Sibiu', 'Timisoara', 'Urziceni', 'Vaslui', 'Zerind']\n" - ] - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "distances = {}\n", "all_cities = []\n", @@ -2336,7 +1235,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "metadata": { "collapsed": true }, @@ -2363,7 +1262,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "metadata": { "collapsed": true }, @@ -2412,7 +1311,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "metadata": { "collapsed": true }, @@ -2431,39 +1330,11 @@ }, { "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['Fagaras',\n", - " 'Neamt',\n", - " 'Iasi',\n", - " 'Vaslui',\n", - " 'Hirsova',\n", - " 'Eforie',\n", - " 'Urziceni',\n", - " 'Bucharest',\n", - " 'Giurgiu',\n", - " 'Pitesti',\n", - " 'Craiova',\n", - " 'Drobeta',\n", - " 'Mehadia',\n", - " 'Lugoj',\n", - " 'Timisoara',\n", - " 'Arad',\n", - " 'Zerind',\n", - " 'Oradea',\n", - " 'Sibiu',\n", - " 'Rimnicu']" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "hill_climbing(tsp)" ] @@ -2587,122 +1458,11 @@ }, { "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
def genetic_algorithm(population, fitness_fn, gene_pool=[0, 1], f_thres=None, ngen=1000, pmut=0.1):\n",
-       "    """[Figure 4.8]"""\n",
-       "    for i in range(ngen):\n",
-       "        population = [mutate(recombine(*select(2, population, fitness_fn)), gene_pool, pmut)\n",
-       "                      for i in range(len(population))]\n",
-       "\n",
-       "        fittest_individual = fitness_threshold(fitness_fn, f_thres, population)\n",
-       "        if fittest_individual:\n",
-       "            return fittest_individual\n",
-       "\n",
-       "\n",
-       "    return argmax(population, key=fitness_fn)\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "psource(genetic_algorithm)" ] @@ -2739,114 +1499,11 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
def recombine(x, y):\n",
-       "    n = len(x)\n",
-       "    c = random.randrange(0, n)\n",
-       "    return x[:c] + y[c:]\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "psource(recombine)" ] @@ -2862,121 +1519,11 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
def mutate(x, gene_pool, pmut):\n",
-       "    if random.uniform(0, 1) >= pmut:\n",
-       "        return x\n",
-       "\n",
-       "    n = len(x)\n",
-       "    g = len(gene_pool)\n",
-       "    c = random.randrange(0, n)\n",
-       "    r = random.randrange(0, g)\n",
-       "\n",
-       "    new_gene = gene_pool[r]\n",
-       "    return x[:c] + [new_gene] + x[c+1:]\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "psource(mutate)" ] @@ -2992,122 +1539,11 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
def init_population(pop_number, gene_pool, state_length):\n",
-       "    """Initializes population for genetic algorithm\n",
-       "    pop_number  :  Number of individuals in population\n",
-       "    gene_pool   :  List of possible values for individuals\n",
-       "    state_length:  The length of each individual"""\n",
-       "    g = len(gene_pool)\n",
-       "    population = []\n",
-       "    for i in range(pop_number):\n",
-       "        new_individual = [gene_pool[random.randrange(0, g)] for j in range(state_length)]\n",
-       "        population.append(new_individual)\n",
-       "\n",
-       "    return population\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "psource(init_population)" ] @@ -3159,7 +1595,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3179,7 +1615,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3205,7 +1641,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3223,7 +1659,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3241,7 +1677,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3266,7 +1702,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3284,7 +1720,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3295,7 +1731,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3314,7 +1750,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3336,7 +1772,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3354,7 +1790,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3372,17 +1808,11 @@ }, { "cell_type": "code", - "execution_count": 44, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['j', 'F', 'm', 'F', 'N', 'i', 'c', 'v', 'm', 'j', 'V', 'o', 'd', 'r', 't', 'V', 'H']\n" - ] - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "print(current_best)" ] @@ -3396,17 +1826,11 @@ }, { "cell_type": "code", - "execution_count": 45, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "jFmFNicvmjVodrtVH\n" - ] - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "current_best_string = ''.join(current_best)\n", "print(current_best_string)" @@ -3425,7 +1849,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3449,7 +1873,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3480,122 +1904,11 @@ }, { "cell_type": "code", - "execution_count": 48, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "

\n", - "\n", - "
def genetic_algorithm(population, fitness_fn, gene_pool=[0, 1], f_thres=None, ngen=1000, pmut=0.1):\n",
-       "    """[Figure 4.8]"""\n",
-       "    for i in range(ngen):\n",
-       "        population = [mutate(recombine(*select(2, population, fitness_fn)), gene_pool, pmut)\n",
-       "                      for i in range(len(population))]\n",
-       "\n",
-       "        fittest_individual = fitness_threshold(fitness_fn, f_thres, population)\n",
-       "        if fittest_individual:\n",
-       "            return fittest_individual\n",
-       "\n",
-       "\n",
-       "    return argmax(population, key=fitness_fn)\n",
-       "
\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "psource(genetic_algorithm)" ] @@ -3609,17 +1922,11 @@ }, { "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Current best: Genetic Algorithm\t\tGeneration: 472\t\tFitness: 17\r" - ] - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "population = init_population(max_population, gene_pool, len(target))\n", "solution, generations = genetic_algorithm_stepwise(population, fitness_fn, gene_pool, f_thres, ngen, mutation_rate)" @@ -3662,7 +1969,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3687,17 +1994,11 @@ }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[['R', 'G', 'G', 'R'], ['R', 'G', 'R', 'R'], ['G', 'R', 'G', 'R'], ['R', 'G', 'R', 'G'], ['G', 'R', 'R', 'G'], ['G', 'R', 'G', 'R'], ['G', 'R', 'R', 'R'], ['R', 'G', 'G', 'G']]\n" - ] - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "population = init_population(8, ['R', 'G'], 4)\n", "print(population)" @@ -3714,7 +2015,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3733,17 +2034,11 @@ }, { "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['R', 'G', 'R', 'G']\n" - ] - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "solution = genetic_algorithm(population, fitness, gene_pool=['R', 'G'])\n", "print(solution)" @@ -3758,17 +2053,11 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "4\n" - ] - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "print(fitness(solution))" ] @@ -3803,17 +2092,11 @@ }, { "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[0, 2, 7, 1, 7, 3, 2, 4], [2, 7, 5, 4, 4, 5, 2, 0], [7, 1, 6, 0, 1, 3, 0, 2], [0, 3, 6, 1, 3, 0, 5, 4], [0, 4, 6, 4, 7, 4, 1, 6]]\n" - ] - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "population = init_population(100, range(8), 8)\n", "print(population[:5])" @@ -3834,7 +2117,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": { "collapsed": true }, @@ -3866,18 +2149,11 @@ }, { "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[5, 0, 6, 3, 7, 4, 1, 3]\n", - "26\n" - ] - } - ], + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "solution = genetic_algorithm(population, fitness, f_thres=25, gene_pool=range(8))\n", "print(solution)\n", @@ -3915,7 +2191,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.4" + "version": "3.6.3" } }, "nbformat": 4, diff --git a/search.py b/search.py index ac834d80c..a80a48c8c 100644 --- a/search.py +++ b/search.py @@ -109,10 +109,10 @@ def expand(self, problem): def child_node(self, problem, action): """[Figure 3.10]""" - next = problem.result(self.state, action) - return Node(next, self, action, + next_node = problem.result(self.state, action) + return Node(next_node, self, action, problem.path_cost(self.path_cost, self.state, - action, next)) + action, next_node)) def solution(self): """Return the sequence of actions to go from the root to this node.""" @@ -163,7 +163,7 @@ def __call__(self, percept): return None return self.seq.pop(0) - def update_state(self, percept): + def update_state(self, state, percept): raise NotImplementedError def formulate_goal(self, state): @@ -182,7 +182,7 @@ def search(self, problem): def tree_search(problem, frontier): """Search through the successors of a problem to find a goal. The argument frontier should be an empty queue. - Don't worry about repeated paths to a state. [Figure 3.7]""" + Repeats infinites in case of loops. [Figure 3.7]""" frontier.append(Node(problem.initial)) while frontier: node = frontier.pop() @@ -195,6 +195,7 @@ def tree_search(problem, frontier): def graph_search(problem, frontier): """Search through the successors of a problem to find a goal. The argument frontier should be an empty queue. + Does not get trapped by loops. If two paths reach a state, only use the first one. [Figure 3.7]""" frontier.append(Node(problem.initial)) explored = set() @@ -225,7 +226,11 @@ def depth_first_graph_search(problem): def breadth_first_search(problem): - """[Figure 3.11]""" + """[Figure 3.11] + Note that this function can be implemented in a + single line as below: + return graph_search(problem, FIFOQueue()) + """ node = Node(problem.initial) if problem.goal_test(node.state): return node @@ -571,10 +576,10 @@ def simulated_annealing(problem, schedule=exp_schedule()): neighbors = current.expand(problem) if not neighbors: return current.state - next = random.choice(neighbors) - delta_e = problem.value(next.state) - problem.value(current.state) + next_choice = random.choice(neighbors) + delta_e = problem.value(next_choice.state) - problem.value(current.state) if delta_e > 0 or probability(math.exp(delta_e / T)): - current = next + current = next_choice def simulated_annealing_full(problem, schedule=exp_schedule()): """ This version returns all the states encountered in reaching @@ -589,10 +594,10 @@ def simulated_annealing_full(problem, schedule=exp_schedule()): neighbors = current.expand(problem) if not neighbors: return current.state - next = random.choice(neighbors) - delta_e = problem.value(next.state) - problem.value(current.state) + next_choice = random.choice(neighbors) + delta_e = problem.value(next_choice.state) - problem.value(current.state) if delta_e > 0 or probability(math.exp(delta_e / T)): - current = next + current = next_choice def and_or_graph_search(problem): """[Figure 4.11]Used when the environment is nondeterministic and completely observable. @@ -730,10 +735,10 @@ def __init__(self, initial, goal, graph): self.graph = graph def actions(self, state): - return self.graph.dict[state].keys() + return self.graph.graph_dict[state].keys() def output(self, state, action): - return self.graph.dict[state][action] + return self.graph.graph_dict[state][action] def h(self, state): """Returns least possible cost to reach a goal for the given state.""" @@ -920,16 +925,16 @@ class Graph: length of the link from A to B. 'Lengths' can actually be any object at all, and nodes can be any hashable object.""" - def __init__(self, dict=None, directed=True): - self.dict = dict or {} + def __init__(self, graph_dict=None, directed=True): + self.graph_dict = graph_dict or {} self.directed = directed if not directed: self.make_undirected() def make_undirected(self): """Make a digraph into an undirected graph by adding symmetric edges.""" - for a in list(self.dict.keys()): - for (b, dist) in self.dict[a].items(): + for a in list(self.graph_dict.keys()): + for (b, dist) in self.graph_dict[a].items(): self.connect1(b, a, dist) def connect(self, A, B, distance=1): @@ -941,13 +946,13 @@ def connect(self, A, B, distance=1): def connect1(self, A, B, distance): """Add a link from A to B of given distance, in one direction only.""" - self.dict.setdefault(A, {})[B] = distance + self.graph_dict.setdefault(A, {})[B] = distance def get(self, a, b=None): """Return a link distance or a dict of {node: distance} entries. .get(a,b) returns the distance or None; .get(a) returns a dict of {node: distance} entries, possibly {}.""" - links = self.dict.setdefault(a, {}) + links = self.graph_dict.setdefault(a, {}) if b is None: return links else: @@ -955,12 +960,15 @@ def get(self, a, b=None): def nodes(self): """Return a list of nodes in the graph.""" - return list(self.dict.keys()) + s1 = set([k for k in self.graph_dict.keys()]) + s2 = set([k2 for v in self.graph_dict.values() for k2, v2 in v.items()]) + nodes = s1.union(s2) + return list(nodes) -def UndirectedGraph(dict=None): +def UndirectedGraph(graph_dict=None): """Build a Graph where every edge (including future ones) goes both ways.""" - return Graph(dict=dict, directed=False) + return Graph(graph_dict = graph_dict, directed=False) def RandomGraph(nodes=list(range(10)), min_links=2, width=400, height=300, @@ -1097,7 +1105,7 @@ def path_cost(self, cost_so_far, A, action, B): def find_min_edge(self): """Find minimum value of edges.""" m = infinity - for d in self.graph.dict.values(): + for d in self.graph.graph_dict.values(): local_min = min(d.values()) m = min(m, local_min) From 14a704b11d342233ea730d07716f57b73dd34e73 Mon Sep 17 00:00:00 2001 From: Nouman Ahmed <35970677+Noumanmufc1@users.noreply.github.com> Date: Thu, 15 Mar 2018 03:57:15 +0500 Subject: [PATCH 003/200] Added air_cargo to planning.ipynb (#835) * Added air_cargo to planning.ipynb * Some style issues --- README.md | 2 +- planning.ipynb | 152 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 112 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 968632477..3ab5777c1 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 9.3 | FOL-FC-Ask | `fol_fc_ask` | [`logic.py`][logic] | Done | | | 9.6 | FOL-BC-Ask | `fol_bc_ask` | [`logic.py`][logic] | Done | | | 9.8 | Append | | | | | -| 10.1 | Air-Cargo-problem | `air_cargo` | [`planning.py`][planning] | Done | | +| 10.1 | Air-Cargo-problem | `air_cargo` | [`planning.py`][planning] | Done | Included | | 10.2 | Spare-Tire-Problem | `spare_tire` | [`planning.py`][planning] | Done | | | 10.3 | Three-Block-Tower | `three_block_tower` | [`planning.py`][planning] | Done | | | 10.7 | Cake-Problem | `have_cake_and_eat_cake_too` | [`planning.py`][planning] | Done | | diff --git a/planning.ipynb b/planning.ipynb index 1054f1ee8..ca648a3a0 100644 --- a/planning.ipynb +++ b/planning.ipynb @@ -23,9 +23,7 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "from planning import *" @@ -51,9 +49,7 @@ { "cell_type": "code", "execution_count": 2, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "%psource Action" @@ -83,9 +79,7 @@ { "cell_type": "code", "execution_count": 3, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "%psource PDDL" @@ -110,9 +104,7 @@ { "cell_type": "code", "execution_count": 4, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "from utils import *\n", @@ -141,9 +133,7 @@ { "cell_type": "code", "execution_count": 5, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "knowledge_base.extend([\n", @@ -163,9 +153,7 @@ { "cell_type": "code", "execution_count": 6, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { @@ -203,9 +191,7 @@ { "cell_type": "code", "execution_count": 7, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "#Sibiu to Bucharest\n", @@ -261,9 +247,7 @@ { "cell_type": "code", "execution_count": 8, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "#Drive\n", @@ -284,9 +268,7 @@ { "cell_type": "code", "execution_count": 9, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "def goal_test(kb):\n", @@ -303,31 +285,119 @@ { "cell_type": "code", "execution_count": 10, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "prob = PDDL(knowledge_base, [fly_s_b, fly_b_s, fly_s_c, fly_c_s, fly_b_c, fly_c_b, drive], goal_test)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Air Cargo Problem:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Air Cargo problem involves loading and unloading of cargo and flying it from place to place. The problem can be with defined with three actions: Load, Unload and Fly. Let us now define an object of `air_cargo` problem:" + ] + }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, + "execution_count": 15, + "metadata": {}, "outputs": [], - "source": [] + "source": [ + "airCargo = air_cargo()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, before taking any actions, we will check the `airCargo` if it has completed the goal it is required to do:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "False\n" + ] + } + ], + "source": [ + "print(airCargo.goal_test())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, it hasn't completed the goal. Now, we define the sequence of actions that it should take in order to achieve\n", + "the goal. Then the `airCargo` acts on each of them." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "solution = [expr(\"Load(C1 , P1, SFO)\"),\n", + " expr(\"Fly(P1, SFO, JFK)\"),\n", + " expr(\"Unload(C1, P1, JFK)\"),\n", + " expr(\"Load(C2, P2, JFK)\"),\n", + " expr(\"Fly(P2, JFK, SFO)\"),\n", + " expr(\"Unload (C2, P2, SFO)\")] \n", + "\n", + "for action in solution:\n", + " airCargo.act(action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As the `airCargo` has taken all the steps it needed in order to achieve the goal, we can now check if it has acheived its goal:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "airCargo.goal_test()" + ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], - "source": [] + "source": [ + "It has now achieved its goal." + ] } ], "metadata": { @@ -346,9 +416,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.4.3" + "version": "3.6.4" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 1 } From 80c48c838fd963093791745ce7aca7a00cc3e662 Mon Sep 17 00:00:00 2001 From: Rahul Goswami Date: Thu, 15 Mar 2018 04:38:03 +0530 Subject: [PATCH 004/200] fixed all instances of issue #833 (#843) * test commit * agents.ipynb * agents.ipynb * Fixed all the instances of issue #833 * minor fix and cleared change in agents.ipynb --- agents.py | 12 ++++++------ csp.py | 4 ++-- knowledge.py | 53 ++++++++++++++++++++++---------------------------- logic.py | 7 ++++--- nlp.py | 2 +- notebook.py | 46 +++++++++++++++++++++---------------------- planning.py | 32 ++++++++++++++++-------------- probability.py | 11 ++++++----- rl.py | 30 ++++++++++++++-------------- text.py | 24 ++++++++++++----------- 10 files changed, 110 insertions(+), 111 deletions(-) diff --git a/agents.py b/agents.py index 9b1ff0d33..eb085757a 100644 --- a/agents.py +++ b/agents.py @@ -96,7 +96,7 @@ def program(percept): self.program = program def can_grab(self, thing): - """Returns True if this agent can grab this thing. + """Return True if this agent can grab this thing. Override for appropriate subclasses of Agent and Thing.""" return False @@ -444,7 +444,7 @@ def move_to(self, thing, destination): return thing.bump def add_thing(self, thing, location=(1, 1), exclude_duplicate_class_items=False): - """Adds things to the world. If (exclude_duplicate_class_items) then the item won't be + """Add things to the world. If (exclude_duplicate_class_items) then the item won't be added if the location has at least one item of the same class.""" if (self.is_inbounds(location)): if (exclude_duplicate_class_items and @@ -809,7 +809,7 @@ def init_world(self, program): self.add_thing(Explorer(program), (1, 1), True) def get_world(self, show_walls=True): - """Returns the items in the world""" + """Return the items in the world""" result = [] x_start, y_start = (0, 0) if show_walls else (1, 1) @@ -826,7 +826,7 @@ def get_world(self, show_walls=True): return result def percepts_from(self, agent, location, tclass=Thing): - """Returns percepts from a given location, + """Return percepts from a given location, and replaces some items with percepts from chapter 7.""" thing_percepts = { Gold: Glitter(), @@ -846,7 +846,7 @@ def percepts_from(self, agent, location, tclass=Thing): return result if len(result) else [None] def percept(self, agent): - """Returns things in adjacent (not diagonal) cells of the agent. + """Return things in adjacent (not diagonal) cells of the agent. Result format: [Left, Right, Up, Down, Center / Current location]""" x, y = agent.location result = [] @@ -907,7 +907,7 @@ def execute_action(self, agent, action): agent.has_arrow = False def in_danger(self, agent): - """Checks if Explorer is in danger (Pit or Wumpus), if he is, kill him""" + """Check if Explorer is in danger (Pit or Wumpus), if he is, kill him""" for thing in self.list_things_at(agent.location): if isinstance(thing, Pit) or (isinstance(thing, Wumpus) and thing.alive): agent.alive = False diff --git a/csp.py b/csp.py index 62772c322..70223acf2 100644 --- a/csp.py +++ b/csp.py @@ -351,7 +351,7 @@ def topological_sort(X, root): def build_topological(node, parent, neighbors, visited, stack, parents): - """Builds the topological sort and the parents of each node in the graph""" + """Build the topological sort and the parents of each node in the graph.""" visited[node] = True for n in neighbors[node]: @@ -427,7 +427,7 @@ def MapColoringCSP(colors, neighbors): different_values_constraint) -def parse_neighbors(neighbors, variables=[]): +def parse_neighbors(neighbors, variables=None): """Convert a string of the form 'X: Y Z; Y: Z' into a dict mapping regions to neighbors. The syntax is a region name followed by a ':' followed by zero or more region names, followed by ';', repeated for diff --git a/knowledge.py b/knowledge.py index 6fe09acd2..2bb12f3b8 100644 --- a/knowledge.py +++ b/knowledge.py @@ -11,13 +11,14 @@ # ______________________________________________________________________________ -def current_best_learning(examples, h, examples_so_far=[]): +def current_best_learning(examples, h, examples_so_far=None): """ [Figure 19.2] The hypothesis is a list of dictionaries, with each dictionary representing a disjunction.""" if not examples: return h + examples_so_far = examples_so_far or [] e = examples[0] if is_consistent(e, h): return current_best_learning(examples[1:], h, examples_so_far + [e]) @@ -95,7 +96,7 @@ def generalizations(examples_so_far, h): def add_or(examples_so_far, h): - """Adds an OR operation to the hypothesis. The AND operations in the disjunction + """Add an OR operation to the hypothesis. The AND operations in the disjunction are generated by the last example (which is the problematic one).""" ors = [] e = examples_so_far[-1] @@ -135,7 +136,7 @@ def version_space_update(V, e): def all_hypotheses(examples): - """Builds a list of all the possible hypotheses""" + """Build a list of all the possible hypotheses""" values = values_table(examples) h_powerset = powerset(values.keys()) hypotheses = [] @@ -148,7 +149,7 @@ def all_hypotheses(examples): def values_table(examples): - """Builds a table with all the possible values for each attribute. + """Build a table with all the possible values for each attribute. Returns a dictionary with keys the attribute names and values a list with the possible values for the corresponding attribute.""" values = defaultdict(lambda: []) @@ -210,7 +211,7 @@ def build_h_combinations(hypotheses): def minimal_consistent_det(E, A): - """Returns a minimal set of attributes which give consistent determination""" + """Return a minimal set of attributes which give consistent determination""" n = len(A) for i in range(n + 1): @@ -220,7 +221,7 @@ def minimal_consistent_det(E, A): def consistent_det(A, E): - """Checks if the attributes(A) is consistent with the examples(E)""" + """Check if the attributes(A) is consistent with the examples(E)""" H = {} for e in E: @@ -235,9 +236,9 @@ def consistent_det(A, E): class FOIL_container(FolKB): - """Holds the kb and other necessary elements required by FOIL""" + """Hold the kb and other necessary elements required by FOIL.""" - def __init__(self, clauses=[]): + def __init__(self, clauses=None): self.const_syms = set() self.pred_syms = set() FolKB.__init__(self, clauses) @@ -251,7 +252,7 @@ def tell(self, sentence): raise Exception("Not a definite clause: {}".format(sentence)) def foil(self, examples, target): - """Learns a list of first-order horn clauses + """Learn a list of first-order horn clauses 'examples' is a tuple: (positive_examples, negative_examples). positive_examples and negative_examples are both lists which contain substitutions.""" clauses = [] @@ -268,10 +269,10 @@ def foil(self, examples, target): return clauses def new_clause(self, examples, target): - """Finds a horn clause which satisfies part of the positive + """Find a horn clause which satisfies part of the positive examples but none of the negative examples. The horn clause is specified as [consequent, list of antecedents] - Return value is the tuple (horn_clause, extended_positive_examples)""" + Return value is the tuple (horn_clause, extended_positive_examples).""" clause = [target, []] # [positive_examples, negative_examples] extended_examples = examples @@ -284,14 +285,14 @@ def new_clause(self, examples, target): return (clause, extended_examples[0]) def extend_example(self, example, literal): - """Generates extended examples which satisfy the literal""" + """Generate extended examples which satisfy the literal.""" # find all substitutions that satisfy literal for s in self.ask_generator(subst(example, literal)): s.update(example) yield s def new_literals(self, clause): - """Generates new literals based on known predicate symbols. + """Generate new literals based on known predicate symbols. Generated literal must share atleast one variable with clause""" share_vars = variables(clause[0]) for l in clause[1]: @@ -304,7 +305,7 @@ def new_literals(self, clause): yield Expr(pred, *[var for var in args]) def choose_literal(self, literals, examples): - """Chooses the best literal based on the information gain""" + """Choose the best literal based on the information gain.""" def gain(l): pre_pos = len(examples[0]) pre_neg = len(examples[1]) @@ -328,8 +329,8 @@ def represents(d): return max(literals, key=gain) def update_examples(self, target, examples, extended_examples): - """Adds to the kb those examples what are represented in extended_examples - List of omitted examples is returned""" + """Add to the kb those examples what are represented in extended_examples + List of omitted examples is returned.""" uncovered = [] for example in examples: def represents(d): @@ -346,7 +347,7 @@ def represents(d): def check_all_consistency(examples, h): - """Check for the consistency of all examples under h""" + """Check for the consistency of all examples under h.""" for e in examples: if not is_consistent(e, h): return False @@ -355,7 +356,7 @@ def check_all_consistency(examples, h): def check_negative_consistency(examples, h): - """Check if the negative examples are consistent under h""" + """Check if the negative examples are consistent under h.""" for e in examples: if e['GOAL']: continue @@ -367,7 +368,7 @@ def check_negative_consistency(examples, h): def disjunction_value(e, d): - """The value of example e under disjunction d""" + """The value of example e under disjunction d.""" for k, v in d.items(): if v[0] == '!': # v is a NOT expression @@ -381,7 +382,7 @@ def disjunction_value(e, d): def guess_value(e, h): - """Guess value of example e under hypothesis h""" + """Guess value of example e under hypothesis h.""" for d in h: if disjunction_value(e, d): return True @@ -394,16 +395,8 @@ def is_consistent(e, h): def false_positive(e, h): - if e["GOAL"] == False: - if guess_value(e, h): - return True - - return False + return guess_value(e, h) and not e["GOAL"] def false_negative(e, h): - if e["GOAL"] == True: - if not guess_value(e, h): - return True - - return False + return e["GOAL"] and not guess_value(e, h) diff --git a/logic.py b/logic.py index 5810e633f..129d281cf 100644 --- a/logic.py +++ b/logic.py @@ -901,10 +901,11 @@ class FolKB(KB): False """ - def __init__(self, initial_clauses=[]): + def __init__(self, initial_clauses=None): self.clauses = [] # inefficient: no indexing - for clause in initial_clauses: - self.tell(clause) + if initial_clauses: + for clause in initial_clauses: + self.tell(clause) def tell(self, sentence): if is_definite_clause(sentence): diff --git a/nlp.py b/nlp.py index ace6de90d..6ad92b6bb 100644 --- a/nlp.py +++ b/nlp.py @@ -272,7 +272,7 @@ def __repr__(self): class Chart: """Class for parsing sentences using a chart data structure. - >>> chart = Chart(E0); + >>> chart = Chart(E0) >>> len(chart.parses('the stench is in 2 2')) 1 """ diff --git a/notebook.py b/notebook.py index ae0976900..4bb53cf1c 100644 --- a/notebook.py +++ b/notebook.py @@ -912,17 +912,17 @@ def show_map(graph_data, node_colors = None): # set the size of the plot plt.figure(figsize=(18,13)) # draw the graph (both nodes and edges) with locations from romania_locations - nx.draw(G, pos = {k : node_positions[k] for k in G.nodes()}, - node_color = [node_colors[node] for node in G.nodes()], linewidths = 0.3, edgecolors = 'k') + nx.draw(G, pos={k: node_positions[k] for k in G.nodes()}, + node_color=[node_colors[node] for node in G.nodes()], linewidths=0.3, edgecolors='k') # draw labels for nodes - node_label_handles = nx.draw_networkx_labels(G, pos = node_label_pos, font_size = 14) + node_label_handles = nx.draw_networkx_labels(G, pos=node_label_pos, font_size=14) # add a white bounding box behind the node labels [label.set_bbox(dict(facecolor='white', edgecolor='none')) for label in node_label_handles.values()] # add edge lables to the graph - nx.draw_networkx_edge_labels(G, pos = node_positions, edge_labels = edge_weights, font_size = 14) + nx.draw_networkx_edge_labels(G, pos=node_positions, edge_labels=edge_weights, font_size=14) # add a legend white_circle = lines.Line2D([], [], color="white", marker='o', markersize=15, markerfacecolor="white") @@ -932,7 +932,7 @@ def show_map(graph_data, node_colors = None): green_circle = lines.Line2D([], [], color="green", marker='o', markersize=15, markerfacecolor="green") plt.legend((white_circle, orange_circle, red_circle, gray_circle, green_circle), ('Un-explored', 'Frontier', 'Currently Exploring', 'Explored', 'Final Solution'), - numpoints=1,prop={'size':16}, loc=(.8,.75)) + numpoints=1, prop={'size':16}, loc=(.8,.75)) # show the plot. No need to use in notebooks. nx.draw will show the graph itself. plt.show() @@ -940,7 +940,7 @@ def show_map(graph_data, node_colors = None): ## helper functions for visualisations def final_path_colors(initial_node_colors, problem, solution): - "returns a node_colors dict of the final path provided the problem and solution" + "Return a node_colors dict of the final path provided the problem and solution." # get initial node colors final_colors = dict(initial_node_colors) @@ -956,7 +956,7 @@ def display_visual(graph_data, user_input, algorithm=None, problem=None): def slider_callback(iteration): # don't show graph for the first time running the cell calling this function try: - show_map(graph_data, node_colors = all_node_colors[iteration]) + show_map(graph_data, node_colors=all_node_colors[iteration]) except: pass def visualize_callback(Visualize): @@ -976,26 +976,26 @@ def visualize_callback(Visualize): #time.sleep(.5) slider = widgets.IntSlider(min=0, max=1, step=1, value=0) - slider_visual = widgets.interactive(slider_callback, iteration = slider) + slider_visual = widgets.interactive(slider_callback, iteration=slider) display(slider_visual) - button = widgets.ToggleButton(value = False) - button_visual = widgets.interactive(visualize_callback, Visualize = button) + button = widgets.ToggleButton(value=False) + button_visual = widgets.interactive(visualize_callback, Visualize=button) display(button_visual) if user_input == True: node_colors = dict(initial_node_colors) if isinstance(algorithm, dict): - assert set(algorithm.keys()).issubset(set(["Breadth First Tree Search", + assert set(algorithm.keys()).issubset({"Breadth First Tree Search", "Depth First Tree Search", "Breadth First Search", "Depth First Graph Search", "Uniform Cost Search", - "A-star Search"])) + "A-star Search"}) - algo_dropdown = widgets.Dropdown(description = "Search algorithm: ", - options = sorted(list(algorithm.keys())), - value = "Breadth First Tree Search") + algo_dropdown = widgets.Dropdown(description="Search algorithm: ", + options=sorted(list(algorithm.keys())), + value="Breadth First Tree Search") display(algo_dropdown) elif algorithm is None: print("No algorithm to run.") @@ -1004,7 +1004,7 @@ def visualize_callback(Visualize): def slider_callback(iteration): # don't show graph for the first time running the cell calling this function try: - show_map(graph_data, node_colors = all_node_colors[iteration]) + show_map(graph_data, node_colors=all_node_colors[iteration]) except: pass @@ -1027,18 +1027,18 @@ def visualize_callback(Visualize): slider.value = i #time.sleep(.5) - start_dropdown = widgets.Dropdown(description = "Start city: ", - options = sorted(list(node_colors.keys())), value = "Arad") + start_dropdown = widgets.Dropdown(description="Start city: ", + options=sorted(list(node_colors.keys())), value="Arad") display(start_dropdown) - end_dropdown = widgets.Dropdown(description = "Goal city: ", - options = sorted(list(node_colors.keys())), value = "Fagaras") + end_dropdown = widgets.Dropdown(description="Goal city: ", + options=sorted(list(node_colors.keys())), value="Fagaras") display(end_dropdown) - button = widgets.ToggleButton(value = False) - button_visual = widgets.interactive(visualize_callback, Visualize = button) + button = widgets.ToggleButton(value=False) + button_visual = widgets.interactive(visualize_callback, Visualize=button) display(button_visual) slider = widgets.IntSlider(min=0, max=1, step=1, value=0) - slider_visual = widgets.interactive(slider_callback, iteration = slider) + slider_visual = widgets.interactive(slider_callback, iteration=slider) display(slider_visual) \ No newline at end of file diff --git a/planning.py b/planning.py index e31c8b3a3..95d7655d1 100644 --- a/planning.py +++ b/planning.py @@ -276,8 +276,8 @@ def find_mutex(self): if negeff in self.next_state_links_neg: for a in self.next_state_links_pos[poseff]: for b in self.next_state_links_neg[negeff]: - if set([a, b]) not in self.mutex: - self.mutex.append(set([a, b])) + if {a, b} not in self.mutex: + self.mutex.append({a, b}) # Interference for posprecond in self.current_state_links_pos: @@ -285,16 +285,16 @@ def find_mutex(self): if negeff in self.next_state_links_neg: for a in self.current_state_links_pos[posprecond]: for b in self.next_state_links_neg[negeff]: - if set([a, b]) not in self.mutex: - self.mutex.append(set([a, b])) + if {a, b} not in self.mutex: + self.mutex.append({a, b}) for negprecond in self.current_state_links_neg: poseff = negprecond if poseff in self.next_state_links_pos: for a in self.next_state_links_pos[poseff]: for b in self.current_state_links_neg[negprecond]: - if set([a, b]) not in self.mutex: - self.mutex.append(set([a, b])) + if {a, b} not in self.mutex: + self.mutex.append({a, b}) # Competing needs for posprecond in self.current_state_links_pos: @@ -302,8 +302,8 @@ def find_mutex(self): if negprecond in self.current_state_links_neg: for a in self.current_state_links_pos[posprecond]: for b in self.current_state_links_neg[negprecond]: - if set([a, b]) not in self.mutex: - self.mutex.append(set([a, b])) + if {a, b} not in self.mutex: + self.mutex.append({a, b}) # Inconsistent support state_mutex = [] @@ -314,7 +314,7 @@ def find_mutex(self): else: next_state_1 = self.next_action_links[list(pair)[0]] if (len(next_state_0) == 1) and (len(next_state_1) == 1): - state_mutex.append(set([next_state_0[0], next_state_1[0]])) + state_mutex.append({next_state_0[0], next_state_1[0]}) self.mutex = self.mutex+state_mutex @@ -565,18 +565,20 @@ class HLA(Action): """ unique_group = 1 - def __init__(self, action, precond=[None, None], effect=[None, None], duration=0, - consume={}, use={}): + def __init__(self, action, precond=None, effect=None, duration=0, + consume=None, use=None): """ As opposed to actions, to define HLA, we have added constraints. duration holds the amount of time required to execute the task consumes holds a dictionary representing the resources the task consumes uses holds a dictionary representing the resources the task uses """ + precond = precond or [None, None] + effect = effect or [None, None] super().__init__(action, precond, effect) self.duration = duration - self.consumes = consume - self.uses = use + self.consumes = consume or {} + self.uses = use or {} self.completed = False # self.priority = -1 # must be assigned in relation to other HLAs # self.job_group = -1 # must be assigned in relation to other HLAs @@ -644,10 +646,10 @@ class Problem(PDDL): This class is identical to PDLL, except that it overloads the act function to handle resource and ordering conditions imposed by HLA as opposed to Action. """ - def __init__(self, initial_state, actions, goal_test, jobs=None, resources={}): + def __init__(self, initial_state, actions, goal_test, jobs=None, resources=None): super().__init__(initial_state, actions, goal_test) self.jobs = jobs - self.resources = resources + self.resources = resources or {} def act(self, action): """ diff --git a/probability.py b/probability.py index 9b732edd7..205ae426e 100644 --- a/probability.py +++ b/probability.py @@ -165,10 +165,11 @@ def enumerate_joint(variables, e, P): class BayesNet: """Bayesian network containing only boolean-variable nodes.""" - def __init__(self, node_specs=[]): + def __init__(self, node_specs=None): """Nodes must be ordered with parents before children.""" self.nodes = [] self.variables = [] + node_specs = node_specs or [] for node_spec in node_specs: self.add(node_spec) @@ -526,10 +527,10 @@ def markov_blanket_sample(X, e, bn): class HiddenMarkovModel: """A Hidden markov model which takes Transition model and Sensor model as inputs""" - def __init__(self, transition_model, sensor_model, prior=[0.5, 0.5]): + def __init__(self, transition_model, sensor_model, prior=None): self.transition_model = transition_model self.sensor_model = sensor_model - self.prior = prior + self.prior = prior or [0.5, 0.5] def sensor_dist(self, ev): if ev is True: @@ -561,10 +562,10 @@ def forward_backward(HMM, ev, prior): t = len(ev) ev.insert(0, None) # to make the code look similar to pseudo code - fv = [[0.0, 0.0] for i in range(len(ev))] + fv = [[0.0, 0.0] for _ in range(len(ev))] b = [1.0, 1.0] bv = [b] # we don't need bv; but we will have a list of all backward messages here - sv = [[0, 0] for i in range(len(ev))] + sv = [[0, 0] for _ in range(len(ev))] fv[0] = prior diff --git a/rl.py b/rl.py index 1b7e20c33..9f9c90676 100644 --- a/rl.py +++ b/rl.py @@ -71,13 +71,13 @@ class ModelMDP(MDP): """ Class for implementing modified Version of input MDP with an editable transition model P and a custom function T. """ def __init__(self, init, actlist, terminals, gamma, states): - super().__init__(init, actlist, terminals, states = states, gamma = gamma) + super().__init__(init, actlist, terminals, states=states, gamma=gamma) nested_dict = lambda: defaultdict(nested_dict) # StackOverflow:whats-the-best-way-to-initialize-a-dict-of-dicts-in-python self.P = nested_dict() def T(self, s, a): - """Returns a list of tuples with probabilities for states + """Return a list of tuples with probabilities for states based on the learnt model P.""" return [(prob, res) for (res, prob) in self.P[(s, a)].items()] @@ -120,8 +120,8 @@ def __call__(self, percept): return self.a def update_state(self, percept): - '''To be overridden in most cases. The default case - assumes the percept to be of type (state, reward)''' + """To be overridden in most cases. The default case + assumes the percept to be of type (state, reward).""" return percept @@ -146,7 +146,7 @@ def __init__(self, pi, mdp, alpha=None): if alpha: self.alpha = alpha else: - self.alpha = lambda n: 1./(1+n) # udacity video + self.alpha = lambda n: 1/(1+n) # udacity video def __call__(self, percept): s1, r1 = self.update_state(percept) @@ -164,8 +164,8 @@ def __call__(self, percept): return self.a def update_state(self, percept): - ''' To be overridden in most cases. The default case - assumes the percept to be of type (state, reward)''' + """To be overridden in most cases. The default case + assumes the percept to be of type (state, reward).""" return percept @@ -202,7 +202,7 @@ def f(self, u, n): return u def actions_in_state(self, state): - """ Returns actions possible in given state. + """ Return actions possible in given state. Useful for max and argmax. """ if state in self.terminals: return [None] @@ -229,21 +229,21 @@ def __call__(self, percept): return self.a def update_state(self, percept): - ''' To be overridden in most cases. The default case - assumes the percept to be of type (state, reward)''' + """To be overridden in most cases. The default case + assumes the percept to be of type (state, reward).""" return percept def run_single_trial(agent_program, mdp): - ''' Execute trial for given agent_program + """Execute trial for given agent_program and mdp. mdp should be an instance of subclass - of mdp.MDP ''' + of mdp.MDP """ def take_single_action(mdp, s, a): - ''' - Selects outcome of taking action a + """ + Select outcome of taking action a in state s. Weighted Sampling. - ''' + """ x = random.uniform(0, 1) cumulative_probability = 0.0 for probability_state in mdp.T(s, a): diff --git a/text.py b/text.py index 8dc0ab855..b6beb28ca 100644 --- a/text.py +++ b/text.py @@ -37,19 +37,19 @@ class NgramWordModel(CountingProbDist): You can add, sample or get P[(word1, ..., wordn)]. The method P.samples(n) builds up an n-word sequence; P.add_cond_prob and P.add_sequence add data.""" - def __init__(self, n, observation_sequence=[], default=0): + def __init__(self, n, observation_sequence=None, default=0): # In addition to the dictionary of n-tuples, cond_prob is a # mapping from (w1, ..., wn-1) to P(wn | w1, ... wn-1) CountingProbDist.__init__(self, default=default) self.n = n self.cond_prob = defaultdict() - self.add_sequence(observation_sequence) + self.add_sequence(observation_sequence or []) # __getitem__, top, sample inherited from CountingProbDist # Note that they deal with tuples, not strings, as inputs def add_cond_prob(self, ngram): - """Builds the conditional probabilities P(wn | (w1, ..., wn-1)""" + """Build the conditional probabilities P(wn | (w1, ..., wn-1)""" if ngram[:-1] not in self.cond_prob: self.cond_prob[ngram[:-1]] = CountingProbDist() self.cond_prob[ngram[:-1]].add(ngram[-1]) @@ -88,14 +88,16 @@ def add_sequence(self, words): class UnigramCharModel(NgramCharModel): - def __init__(self, observation_sequence=[], default=0): + def __init__(self, observation_sequence=None, default=0): CountingProbDist.__init__(self, default=default) self.n = 1 self.cond_prob = defaultdict() - self.add_sequence(observation_sequence) + self.add_sequence(observation_sequence or []) def add_sequence(self, words): - [self.add(char) for word in words for char in list(word)] + for word in words: + for char in word: + self.add(char) # ______________________________________________________________________________ @@ -368,9 +370,9 @@ def decode(self, ciphertext): """Search for a decoding of the ciphertext.""" self.ciphertext = canonicalize(ciphertext) # reduce domain to speed up search - self.chardomain = {c for c in self.ciphertext if c is not ' '} + self.chardomain = {c for c in self.ciphertext if c != ' '} problem = PermutationDecoderProblem(decoder=self) - solution = search.best_first_graph_search( + solution = search.best_first_graph_search( problem, lambda node: self.score(node.state)) solution.state[' '] = ' ' @@ -388,9 +390,9 @@ def score(self, code): # add small positive value to prevent computing log(0) # TODO: Modify the values to make score more accurate - logP = (sum([log(self.Pwords[word] + 1e-20) for word in words(text)]) + - sum([log(self.P1[c] + 1e-5) for c in text]) + - sum([log(self.P2[b] + 1e-10) for b in bigrams(text)])) + logP = (sum(log(self.Pwords[word] + 1e-20) for word in words(text)) + + sum(log(self.P1[c] + 1e-5) for c in text) + + sum(log(self.P2[b] + 1e-10) for b in bigrams(text))) return -exp(logP) From e3270d0477a35c38e03c41ed6d8ab8e4794cfe07 Mon Sep 17 00:00:00 2001 From: Aman Deep Singh Date: Thu, 15 Mar 2018 04:50:06 +0530 Subject: [PATCH 005/200] Added min-conflicts section (#841) * Added section on min-conflicts * Refactor one-liner for loop * Added tests for min_conflicts and NQueensCSP --- csp.ipynb | 604 ++++++++++++++++++++++++++++++++++++++++++++-- tests/test_csp.py | 55 +++++ 2 files changed, 641 insertions(+), 18 deletions(-) diff --git a/csp.ipynb b/csp.ipynb index 1de9e1312..be3882387 100644 --- a/csp.ipynb +++ b/csp.ipynb @@ -52,7 +52,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "psource(CSP)" @@ -105,7 +107,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "psource(different_values_constraint)" @@ -139,7 +143,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "psource(MapColoringCSP)" @@ -178,9 +184,114 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def queen_constraint(A, a, B, b):\n",
+       "    """Constraint is satisfied (true) if A, B are really the same variable,\n",
+       "    or if they are not in the same row, down diagonal, or up diagonal."""\n",
+       "    return A == B or (a != b and A + a != B + b and A - a != B - b)\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(queen_constraint)" ] @@ -194,9 +305,191 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
class NQueensCSP(CSP):\n",
+       "    """Make a CSP for the nQueens problem for search with min_conflicts.\n",
+       "    Suitable for large n, it uses only data structures of size O(n).\n",
+       "    Think of placing queens one per column, from left to right.\n",
+       "    That means position (x, y) represents (var, val) in the CSP.\n",
+       "    The main structures are three arrays to count queens that could conflict:\n",
+       "        rows[i]      Number of queens in the ith row (i.e val == i)\n",
+       "        downs[i]     Number of queens in the \\ diagonal\n",
+       "                     such that their (x, y) coordinates sum to i\n",
+       "        ups[i]       Number of queens in the / diagonal\n",
+       "                     such that their (x, y) coordinates have x-y+n-1 = i\n",
+       "    We increment/decrement these counts each time a queen is placed/moved from\n",
+       "    a row/diagonal. So moving is O(1), as is nconflicts.  But choosing\n",
+       "    a variable, and a best value for the variable, are each O(n).\n",
+       "    If you want, you can keep track of conflicted variables, then variable\n",
+       "    selection will also be O(1).\n",
+       "    >>> len(backtracking_search(NQueensCSP(8)))\n",
+       "    8\n",
+       "    """\n",
+       "\n",
+       "    def __init__(self, n):\n",
+       "        """Initialize data structures for n Queens."""\n",
+       "        CSP.__init__(self, list(range(n)), UniversalDict(list(range(n))),\n",
+       "                     UniversalDict(list(range(n))), queen_constraint)\n",
+       "\n",
+       "        self.rows = [0]*n\n",
+       "        self.ups = [0]*(2*n - 1)\n",
+       "        self.downs = [0]*(2*n - 1)\n",
+       "\n",
+       "    def nconflicts(self, var, val, assignment):\n",
+       "        """The number of conflicts, as recorded with each assignment.\n",
+       "        Count conflicts in row and in up, down diagonals. If there\n",
+       "        is a queen there, it can't conflict with itself, so subtract 3."""\n",
+       "        n = len(self.variables)\n",
+       "        c = self.rows[val] + self.downs[var+val] + self.ups[var-val+n-1]\n",
+       "        if assignment.get(var, None) == val:\n",
+       "            c -= 3\n",
+       "        return c\n",
+       "\n",
+       "    def assign(self, var, val, assignment):\n",
+       "        """Assign var, and keep track of conflicts."""\n",
+       "        oldval = assignment.get(var, None)\n",
+       "        if val != oldval:\n",
+       "            if oldval is not None:  # Remove old val if there was one\n",
+       "                self.record_conflict(assignment, var, oldval, -1)\n",
+       "            self.record_conflict(assignment, var, val, +1)\n",
+       "            CSP.assign(self, var, val, assignment)\n",
+       "\n",
+       "    def unassign(self, var, assignment):\n",
+       "        """Remove var from assignment (if it is there) and track conflicts."""\n",
+       "        if var in assignment:\n",
+       "            self.record_conflict(assignment, var, assignment[var], -1)\n",
+       "        CSP.unassign(self, var, assignment)\n",
+       "\n",
+       "    def record_conflict(self, assignment, var, val, delta):\n",
+       "        """Record conflicts caused by addition or deletion of a Queen."""\n",
+       "        n = len(self.variables)\n",
+       "        self.rows[val] += delta\n",
+       "        self.downs[var + val] += delta\n",
+       "        self.ups[var - val + n - 1] += delta\n",
+       "\n",
+       "    def display(self, assignment):\n",
+       "        """Print the queens and the nconflicts values (for debugging)."""\n",
+       "        n = len(self.variables)\n",
+       "        for val in range(n):\n",
+       "            for var in range(n):\n",
+       "                if assignment.get(var, '') == val:\n",
+       "                    ch = 'Q'\n",
+       "                elif (var + val) % 2 == 0:\n",
+       "                    ch = '.'\n",
+       "                else:\n",
+       "                    ch = '-'\n",
+       "                print(ch, end=' ')\n",
+       "            print('    ', end=' ')\n",
+       "            for var in range(n):\n",
+       "                if assignment.get(var, '') == val:\n",
+       "                    ch = '*'\n",
+       "                else:\n",
+       "                    ch = ' '\n",
+       "                print(str(self.nconflicts(var, val, assignment)) + ch, end=' ')\n",
+       "            print()\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "psource(NQueensCSP)" ] @@ -210,7 +503,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": { "collapsed": true }, @@ -219,6 +512,275 @@ "eight_queens = NQueensCSP(8)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have defined our CSP. \n", + "We now need to solve this.\n", + "\n", + "### Min-conflicts\n", + "As stated above, the `min_conflicts` algorithm is an efficient method to solve such a problem.\n", + "
\n", + "To begin with, all the variables of the CSP are _randomly_ initialized. \n", + "
\n", + "The algorithm then randomly selects a variable that has conflicts and violates some constraints of the CSP.\n", + "
\n", + "The selected variable is then assigned a value that _minimizes_ the number of conflicts.\n", + "
\n", + "This is a simple stochastic algorithm which works on a principle similar to **Hill-climbing**.\n", + "The conflicting state is repeatedly changed into a state with fewer conflicts in an attempt to reach an approximate solution.\n", + "
\n", + "This algorithm sometimes benefits from having a good initial assignment.\n", + "Using greedy techniques to get a good initial assignment and then using `min_conflicts` to solve the CSP can speed up the procedure dramatically, especially for CSPs with a large state space." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def min_conflicts(csp, max_steps=100000):\n",
+       "    """Solve a CSP by stochastic hillclimbing on the number of conflicts."""\n",
+       "    # Generate a complete assignment for all variables (probably with conflicts)\n",
+       "    csp.current = current = {}\n",
+       "    for var in csp.variables:\n",
+       "        val = min_conflicts_value(csp, var, current)\n",
+       "        csp.assign(var, val, current)\n",
+       "    # Now repeatedly choose a random conflicted variable and change it\n",
+       "    for i in range(max_steps):\n",
+       "        conflicted = csp.conflicted_vars(current)\n",
+       "        if not conflicted:\n",
+       "            return current\n",
+       "        var = random.choice(conflicted)\n",
+       "        val = min_conflicts_value(csp, var, current)\n",
+       "        csp.assign(var, val, current)\n",
+       "    return None\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(min_conflicts)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's use this algorithm to solve the `eight_queens` CSP." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "solution = min_conflicts(eight_queens)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is indeed a valid solution. \n", + "Let's write a helper function to visualize the solution space." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "%matplotlib inline\n", + "\n", + "def display_NQueensCSP(solution):\n", + " n = len(solution)\n", + " board = np.array([2 * int((i + j) % 2) for j in range(n) for i in range(n)]).reshape((n, n))\n", + " \n", + " for (k, v) in solution.items():\n", + " board[k][v] = 1\n", + " \n", + " fig = plt.figure(figsize=(7, 7))\n", + " ax = fig.add_subplot(111)\n", + " ax.set_title(f'{n} Queens')\n", + " plt.imshow(board, cmap='binary', interpolation='nearest')\n", + " ax.set_aspect('equal')\n", + " fig.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAeAAAAHwCAYAAAB+ArwOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4wLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvpW3flQAAFZFJREFUeJzt3HuspAd53/HfE6+52DFxG7bUFwpE\njSxR1AB7IEWuaIshsQMlVS+SaYNCVNVpGxLcRk1J/tmlSqU2f0SkokXZGAhJAItrRRGYECU0RW0M\nZ40pGEMFxhGLcbxu4hpwg7Hz9I8zbpdllzPbzOzjM+fzkY58Zuad9zzj18ff815mqrsDAJxb3zE9\nAADsRwIMAAMEGAAGCDAADBBgABggwAAwQIABYIAAA8AAAYZzoKqeWlXvr6o/qqq7q+p1VXXg2yx/\ncVW9frHsA1X1yar60XM5M7BeAgznxn9Ick+SS5I8M8lfS/JPT7dgVT0myW8leUqS5yX5riT/Iskv\nVNVPnZNpgbUTYDg3npbk7d39x919d5KbkvylMyz78iR/Icnf6+4vdPc3uvumJD+V5Oer6qIkqaqu\nqr/4yJOq6ler6udPuv2Sqrq1qu6rqv9aVX/5pMcurap3VdWJqvrCyWGvqiNV9faq+rWq+kpV3VZV\nWyc9/i+r6kuLxz5bVVet5l8R7C8CDOfGLyW5tqouqKrLklyTnQifzouSfKC7v3bK/e9KckGSv7Lb\nD6uqZyd5Y5IfT/LdSX45yXur6rFV9R1J/lOSTyS5LMlVSa6vqh88aRUvTXJjkouTvDfJ6xbrvSLJ\nK5M8p7svSvKDSe7cbR7gWwkwnBv/OTt7vPcnOZ5kO8l/PMOyT0zy5VPv7O6Hktyb5OASP+8fJfnl\n7r65ux/u7jcn+Xp24v2cJAe7+19194PdfUeSX0ly7UnP/0h3v7+7H07y60m+b3H/w0kem+TpVXV+\nd9/Z3Z9fYh7gFAIMa7bY4/xgkncnuTA7gf0zSf7tGZ5yb3bOFZ+6ngOL555Y4sc+JclPLw4/31dV\n9yV5cpJLF49despjP5fkSSc9/+6Tvn8gyeOq6kB3fy7J9UmOJLmnqm6sqkuXmAc4hQDD+v3Z7MTv\ndd399e7+n0nelOSHzrD8byW5pqouPOX+v5PkG0k+urj9QHYOST/iz5/0/ReT/Ovuvvikrwu6+22L\n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\nIMAAMECAAWCAAAPAAAEGgAECDAADBBgABggwAAwQYAAYIMAAMECAAWCAAAPAAAEGgAECDAADBBgA\nBggwAAwQYAAYIMAAMKC6e/UrrTqR5PdXvuIze2KSe8/hzzvXvL69zevb+zb9NXp9q/WU7j6420Jr\nCfC5VlXb3b01Pce6eH17m9e39236a/T6ZjgEDQADBBgABmxKgI9OD7BmXt/e5vXtfZv+Gr2+ARtx\nDhgA9ppN2QMGgD1FgAFgwJ4OcFVdXVWfrarPVdWrp+dZtap6Y1XdU1Wfmp5lHarqyVX1O1V1e1Xd\nVlWvmp5plarqcVX10ar6xOL1vWZ6pnWoqvOq6uNV9b7pWVatqu6sqk9W1a1VtT09z6pV1cVV9c6q\n+szi9/B50zOtUlVdsdh2j3zdX1XXT8/1iD17DriqzkvyP5K8KMnxJB9L8rLu/vToYCtUVc9P8tUk\nv9bdz5ieZ9Wq6pIkl3T3LVV1UZJjSf7WpmzDqqokF3b3V6vq/CQfSfKq7v694dFWqqr+eZKtJE/o\n7pdMz7NKVXVnkq3u3sgPqaiqNyf5L919Q1U9JskF3X3f9FzrsGjGl5J8f3efyw+KOqO9vAf83CSf\n6+47uvvBJDcm+eHhmVaqu383yR9Oz7Eu3f3l7r5l8f1Xktye5LLZqVand3x1cfP8xdfe/Iv3DKrq\n8iQvTnLD9Cycnap6QpLnJ3lDknT3g5sa34Wrknz+0RLfZG8H+LIkXzzp9vFs0P+895uqemqSZyW5\neXaS1Vocnr01yT1JPtTdG/X6krw2yc8k+ZPpQdakk/xmVR2rquumh1mx70lyIsmbFqcQbqiqC6eH\nWqNrk7xteoiT7eUA12nu26i9i/2iqr4zybuSXN/d90/Ps0rd/XB3PzPJ5UmeW1Ubcyqhql6S5J7u\nPjY9yxpd2d3PTnJNkp9YnBbaFAeSPDvJ67v7WUm+lmTjrqVJksXh9Zcmecf0LCfbywE+nuTJJ92+\nPMldQ7Pw/2lxbvRdSd7S3e+enmddFof2Ppzk6uFRVunKJC9dnCe9MckLquo3Zkdare6+a/HPe5K8\nJzunvjbF8STHTzoq887sBHkTXZPklu7+g+lBTraXA/yxJN9bVU9b/HVzbZL3Ds/EWVhcpPSGJLd3\n9y9Oz7NqVXWwqi5efP/4JC9M8pnZqVanu3+2uy/v7qdm5/fvt7v7R4bHWpmqunBxcWAWh2Z/IMnG\nvCOhu+9O8sWqumJx11VJNuICyNN4WR5lh5+TnUMQe1J3P1RVr0zywSTnJXljd982PNZKVdXbkvz1\nJE+squNJDnf3G2anWqkrk7w8yScX50mT5Oe6+/2DM63SJUnevLj68juSvL27N+6tOhvsSUnes/N3\nYg4keWt33zQ70sr9ZJK3LHZi7kjyY8PzrFxVXZCdd8v8+PQsp9qzb0MCgL1sLx+CBoA9S4ABYIAA\nA8AAAQaAAQIMAAMEGAAGCDAADPg/v2hxZuiP1asAAAAASUVORK5CYII=\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display_NQueensCSP(solution)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The gray cells indicate the positions of the queens." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lets' see if we can find a different solution." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAeAAAAHwCAYAAAB+ArwOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4wLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvpW3flQAAFaFJREFUeJzt3G2spAd53+H/Ha95sWPiNmwptikQ\nNbJEUQPsgRS5oi2GxA6UVH2RTBsUoqpO25DgNmpK8mWXKpXafIhIRYviGAhJAIvXilpgQpTQFLUx\nnDWmYAwVGEcsi+N1E9eAG4ydux/OuF2WXc5sM7O3z5zrko72zMwzz7nHj8a/87zMqe4OAHBufcf0\nAACwHwkwAAwQYAAYIMAAMECAAWCAAAPAAAEGgAECDAADBBjOgap6WlW9v6r+qKrurqrXV9WBb7P8\nxVX1hsWyD1TVJ6vqR8/lzMB6CTCcG/8hyT1JnpzkWUn+WpJ/eroFq+oxSX4ryVOTPD/JdyX5F0l+\noap+6pxMC6ydAMO58fQk7+juP+7uu5PcnOQvnWHZVyT5C0n+Xnd/obu/0d03J/mpJD9fVRclSVV1\nVf3FR55UVb9aVT9/0u2XVtVtVXVfVf3XqvrLJz12SVW9u6pOVNUXTg57VR2pqndU1a9V1Veq6vaq\n2jrp8X9ZVV9aPPbZqrpyNf+JYH8RYDg3finJNVV1QVVdmuTq7ET4dF6c5APd/bVT7n93kguS/JXd\nflhVPSfJm5L8eJLvTvLLSd5XVY+tqu9I8p+SfCLJpUmuTHJdVf3gSat4WZIbk1yc5H1JXr9Y7+VJ\nXpXkud19UZIfTHLXbvMA30qA4dz4z9nZ470/ybEk20n+4xmWfWKSL596Z3c/lOTeJAeX+Hn/KMkv\nd/ct3f1wd78lydezE+/nJjnY3f+qux/s7juT/EqSa056/ke6+/3d/XCSX0/yfYv7H07y2CTPqKrz\nu/uu7v78EvMApxBgWLPFHucHk7wnyYXZCeyfSfJvz/CUe7NzrvjU9RxYPPfEEj/2qUl+enH4+b6q\nui/JU5JcsnjsklMe+7kkTzrp+Xef9P0DSR5XVQe6+3NJrktyJMk9VXVjVV2yxDzAKQQY1u/PZid+\nr+/ur3f3/0zy5iQ/dIblfyvJ1VV14Sn3/50k30jy0cXtB7JzSPoRf/6k77+Y5F9398UnfV3Q3W9f\n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\nAgwAAwQYAAYIMAAMEGAAGCDAADBAgAFggAADwAABBoABAgwAAwQYAAYIMAAMEGAAGCDAADBAgAFg\ngAADwAABBoABAgwAAwQYAAYIMAAMEGAAGCDAADCgunv1K606keT3V77iM3tiknvP4c8717y+vc3r\n2/s2/TV6fav11O4+uNtCawnwuVZV2929NT3Hunh9e5vXt/dt+mv0+mY4BA0AAwQYAAZsSoCvnx5g\nzby+vc3r2/s2/TV6fQM24hwwAOw1m7IHDAB7igADwIA9HeCquqqqPltVn6uq10zPs2pV9aaquqeq\nPjU9yzpU1VOq6neq6o6qur2qXj090ypV1eOq6qNV9YnF63vt9EzrUFXnVdXHq+qm6VlWraruqqpP\nVtVtVbU9Pc+qVdXFVfWuqvrM4n34/OmZVqmqLl9su0e+7q+q66bnesSePQdcVecl+R9JXpzkWJKP\nJXl5d396dLAVqqoXJPlqkl/r7mdOz7NqVfXkJE/u7lur6qIkR5P8rU3ZhlVVSS7s7q9W1flJPpLk\n1d39e8OjrVRV/fMkW0me0N0vnZ5nlarqriRb3b2Rf6Siqt6S5L909w1V9ZgkF3T3fdNzrcOiGV9K\n8v3dfS7/UNQZ7eU94Ocl+Vx339ndDya5MckPD8+0Ut39u0n+cHqOdenuL3f3rYvvv5LkjiSXzk61\nOr3jq4ub5y++9uZvvGdQVZcleUmSG6Zn4exU1ROSvCDJG5Okux/c1PguXJnk84+W+CZ7O8CXJvni\nSbePZYP+573fVNXTkjw7yS2zk6zW4vDsbUnuSfKh7t6o15fkdUl+JsmfTA+yJp3kN6vqaFVdOz3M\nin1PkhNJ3rw4hXBDVV04PdQaXZPk7dNDnGwvB7hOc99G7V3sF1X1nUneneS67r5/ep5V6u6Hu/tZ\nSS5L8ryq2phTCVX10iT3dPfR6VnW6Irufk6Sq5P8xOK00KY4kOQ5Sd7Q3c9O8rUkG3ctTZIsDq+/\nLMk7p2c52V4O8LEkTznp9mVJjg/Nwv+nxbnRdyd5a3e/Z3qedVkc2vtwkquGR1mlK5K8bHGe9MYk\nL6yq35gdabW6+/ji33uSvDc7p742xbEkx046KvOu7AR5E12d5Nbu/oPpQU62lwP8sSTfW1VPX/x2\nc02S9w3PxFlYXKT0xiR3dPcvTs+zalV1sKouXnz/+CQvSvKZ2alWp7t/trsv6+6nZef999vd/SPD\nY61MVV24uDgwi0OzP5BkYz6R0N13J/liVV2+uOvKJBtxAeRpvDyPssPPyc4hiD2pux+qqlcl+WCS\n85K8qbtvHx5rparq7Un+epInVtWxJIe7+42zU63UFUlekeSTi/OkSfJz3f3+wZlW6clJ3rK4+vI7\nkryjuzfuozob7ElJ3rvze2IOJHlbd988O9LK/WSSty52Yu5M8mPD86xcVV2QnU/L/Pj0LKfasx9D\nAoC9bC8fggaAPUuAAWCAAAPAAAEGgAECDAADBBgABggwAAz4PyWycpsM6xLVAAAAAElFTkSuQmCC\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "eight_queens = NQueensCSP(8)\n", + "solution = min_conflicts(eight_queens)\n", + "display_NQueensCSP(solution)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The solution is a bit different this time. \n", + "Running the above cell several times should give you various valid solutions.\n", + "
\n", + "In the `search.ipynb` notebook, we will see how NQueensProblem can be solved using a heuristic search method such as `uniform_cost_search` and `astar_search`." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -466,7 +1028,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "psource(mrv)" @@ -475,7 +1039,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "psource(num_legal_values)" @@ -484,7 +1050,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "psource(CSP.nconflicts)" @@ -500,7 +1068,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "psource(lcv)" @@ -663,7 +1233,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "psource(tree_csp_solver)" @@ -1162,11 +1734,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.3" - }, - "widgets": { - "state": {}, - "version": "1.1.1" + "version": "3.6.1" } }, "nbformat": 4, diff --git a/tests/test_csp.py b/tests/test_csp.py index f63e657aa..0f282e3fe 100644 --- a/tests/test_csp.py +++ b/tests/test_csp.py @@ -351,6 +351,61 @@ def test_min_conflicts(): australia_impossible = MapColoringCSP(list('RG'), 'SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: ') assert min_conflicts(australia_impossible, 1000) is None + assert min_conflicts(NQueensCSP(2), 1000) is None + assert min_conflicts(NQueensCSP(3), 1000) is None + + +def test_nqueens_csp(): + csp = NQueensCSP(8) + + assignment = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4} + csp.assign(5, 5, assignment) + assert len(assignment) == 6 + csp.assign(6, 6, assignment) + assert len(assignment) == 7 + csp.assign(7, 7, assignment) + assert len(assignment) == 8 + assert assignment[5] == 5 + assert assignment[6] == 6 + assert assignment[7] == 7 + assert csp.nconflicts(3, 2, assignment) == 0 + assert csp.nconflicts(3, 3, assignment) == 0 + assert csp.nconflicts(1, 5, assignment) == 1 + assert csp.nconflicts(7, 5, assignment) == 2 + csp.unassign(1, assignment) + csp.unassign(2, assignment) + csp.unassign(3, assignment) + assert 1 not in assignment + assert 2 not in assignment + assert 3 not in assignment + + assignment = {} + assignment = {0: 0, 1: 1, 2: 4, 3: 1, 4: 6} + csp.assign(5, 7, assignment) + assert len(assignment) == 6 + csp.assign(6, 6, assignment) + assert len(assignment) == 7 + csp.assign(7, 2, assignment) + assert len(assignment) == 8 + assert assignment[5] == 7 + assert assignment[6] == 6 + assert assignment[7] == 2 + assignment = {0: 0, 1: 1, 2: 4, 3: 1, 4: 6, 5: 7, 6: 6, 7: 2} + assert csp.nconflicts(7, 7, assignment) == 4 + assert csp.nconflicts(3, 4, assignment) == 0 + assert csp.nconflicts(2, 6, assignment) == 2 + assert csp.nconflicts(5, 5, assignment) == 3 + csp.unassign(4, assignment) + csp.unassign(5, assignment) + csp.unassign(6, assignment) + assert 4 not in assignment + assert 5 not in assignment + assert 6 not in assignment + + for n in range(5, 9): + csp = NQueensCSP(n) + solution = min_conflicts(csp) + assert not solution or sorted(solution.values()) == list(range(n)) def test_universal_dict(): From fea29d195d6cab515d487973bba841c12d7e0ae2 Mon Sep 17 00:00:00 2001 From: Aabir Abubaker Kar <16526730+bakerwho@users.noreply.github.com> Date: Wed, 14 Mar 2018 19:38:05 -0400 Subject: [PATCH 006/200] Rewrote parts of search.ipynb (#809) * Rewrote parts of search.ipynb * Fixed typo and cleared cell output --- search-4e.ipynb | 3 ++- search.ipynb | 48 ++++++++++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/search-4e.ipynb b/search-4e.ipynb index c2d0dae61..1912a7fa8 100644 --- a/search-4e.ipynb +++ b/search-4e.ipynb @@ -1929,6 +1929,7 @@ "execution_count": 52, "metadata": { "button": false, + "collapsed": true, "new_sheet": false, "run_control": { "read_only": false @@ -3822,7 +3823,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.3" + "version": "3.6.1" }, "widgets": { "state": {}, diff --git a/search.ipynb b/search.ipynb index 1ac4b075a..718161391 100644 --- a/search.ipynb +++ b/search.ipynb @@ -54,22 +54,24 @@ "source": [ "## OVERVIEW\n", "\n", - "Here, we learn about problem solving. Building goal-based agents that can plan ahead to solve problems, in particular, navigation problem/route finding problem. First, we will start the problem solving by precisely defining **problems** and their **solutions**. We will look at several general-purpose search algorithms. Broadly, search algorithms are classified into two types:\n", + "Here, we learn about a specific kind of problem solving - building goal-based agents that can plan ahead to solve problems. In particular, we examine navigation problem/route finding problem. We must begin by precisely defining **problems** and their **solutions**. We will look at several general-purpose search algorithms.\n", + "\n", + "Search algorithms can be classified into two types:\n", "\n", "* **Uninformed search algorithms**: Search algorithms which explore the search space without having any information about the problem other than its definition.\n", - "* Examples:\n", - " 1. Breadth First Search\n", - " 2. Depth First Search\n", - " 3. Depth Limited Search\n", - " 4. Iterative Deepening Search\n", + " * Examples:\n", + " 1. Breadth First Search\n", + " 2. Depth First Search\n", + " 3. Depth Limited Search\n", + " 4. Iterative Deepening Search\n", "\n", "\n", "* **Informed search algorithms**: These type of algorithms leverage any information (heuristics, path cost) on the problem to search through the search space to find the solution efficiently.\n", - "* Examples:\n", - " 1. Best First Search\n", - " 2. Uniform Cost Search\n", - " 3. A\\* Search\n", - " 4. Recursive Best First Search\n", + " * Examples:\n", + " 1. Best First Search\n", + " 2. Uniform Cost Search\n", + " 3. A\\* Search\n", + " 4. Recursive Best First Search\n", "\n", "*Don't miss the visualisations of these algorithms solving the route-finding problem defined on Romania map at the end of this notebook.*" ] @@ -124,7 +126,7 @@ "source": [ "The `Problem` class has six methods.\n", "\n", - "* `__init__(self, initial, goal)` : This is what is called a `constructor` and is the first method called when you create an instance of the class. `initial` specifies the initial state of our search problem. It represents the start state from where our agent begins its task of exploration to find the goal state(s) which is given in the `goal` parameter.\n", + "* `__init__(self, initial, goal)` : This is what is called a `constructor`. It is the first method called when you create an instance of the class as `Problem(initial, goal)`. The variable `initial` specifies the initial state $s_0$ of the search problem. It represents the beginning state. From here, our agent begins its task of exploration to find the goal state(s) which is given in the `goal` parameter.\n", "\n", "\n", "* `actions(self, state)` : This method returns all the possible actions agent can execute in the given state `state`.\n", @@ -133,7 +135,7 @@ "* `result(self, state, action)` : This returns the resulting state if action `action` is taken in the state `state`. This `Problem` class only deals with deterministic outcomes. So we know for sure what every action in a state would result to.\n", "\n", "\n", - "* `goal_test(self, state)` : Given a graph state, it checks if it is a terminal state. If the state is indeed a goal state, value of `True` is returned. Else, of course, `False` is returned.\n", + "* `goal_test(self, state)` : Return a boolean for a given state - `True` if it is a goal state, else `False`.\n", "\n", "\n", "* `path_cost(self, c, state1, action, state2)` : Return the cost of the path that arrives at `state2` as a result of taking `action` from `state1`, assuming total cost of `c` to get up to `state1`.\n", @@ -164,13 +166,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The `Node` class has nine methods.\n", + "The `Node` class has nine methods. The first is the `__init__` method.\n", "\n", "* `__init__(self, state, parent, action, path_cost)` : This method creates a node. `parent` represents the node that this is a successor of and `action` is the action required to get from the parent node to this node. `path_cost` is the cost to reach current node from parent node.\n", "\n", - "* `__repr__(self)` : This returns the state of this node.\n", - "\n", - "* `__lt__(self, node)` : Given a `node`, this method returns `True` if the state of current node is less than the state of the `node`. Otherwise it returns `False`.\n", + "The next 4 methods are specific `Node`-related functions.\n", "\n", "* `expand(self, problem)` : This method lists all the neighbouring(reachable in one step) nodes of current node. \n", "\n", @@ -180,6 +180,12 @@ "\n", "* `path(self)` : This returns a list of all the nodes that lies in the path from the root to this node.\n", "\n", + "The remaining 4 methods override standards Python functionality for representing an object as a string, the less-than ($<$) operator, the equal-to ($=$) operator, and the `hash` function.\n", + "\n", + "* `__repr__(self)` : This returns the state of this node.\n", + "\n", + "* `__lt__(self, node)` : Given a `node`, this method returns `True` if the state of current node is less than the state of the `node`. Otherwise it returns `False`.\n", + "\n", "* `__eq__(self, other)` : This method returns `True` if the state of current node is equal to the other node. Else it returns `False`.\n", "\n", "* `__hash__(self)` : This returns the hash of the state of current node." @@ -205,7 +211,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now it's time to define our problem. We will define it by passing `initial`, `goal`, `graph` to `GraphProblem`. So, our problem is to find the goal state starting from the given initial state on the provided graph. Have a look at our romania_map, which is an Undirected Graph containing a dict of nodes as keys and neighbours as values." + "Have a look at our romania_map, which is an Undirected Graph containing a dict of nodes as keys and neighbours as values." ] }, { @@ -252,7 +258,9 @@ "And `romania_map.locations` contains the positions of each of the nodes. We will use the straight line distance (which is different from the one provided in `romania_map`) between two cities in algorithms like A\\*-search and Recursive Best First Search.\n", "\n", "**Define a problem:**\n", - "Hmm... say we want to start exploring from **Arad** and try to find **Bucharest** in our romania_map. So, this is how we do it." + "Now it's time to define our problem. We will define it by passing `initial`, `goal`, `graph` to `GraphProblem`. So, our problem is to find the goal state starting from the given initial state on the provided graph. \n", + "\n", + "Say we want to start exploring from **Arad** and try to find **Bucharest** in our romania_map. So, this is how we do it." ] }, { @@ -377,7 +385,7 @@ "source": [ "The SimpleProblemSolvingAgentProgram class has six methods: \n", "\n", - "* `__init__(self, intial_state=None)`: This is the `contructor` of the class and is the first method to be called when the class is instantiated. It takes in a keyword argument, `initial_state` which is initially `None`. The argument `intial_state` represents the state from which the agent starts.\n", + "* `__init__(self, intial_state=None)`: This is the `contructor` of the class and is the first method to be called when the class is instantiated. It takes in a keyword argument, `initial_state` which is initially `None`. The argument `initial_state` represents the state from which the agent starts.\n", "\n", "* `__call__(self, percept)`: This method updates the `state` of the agent based on its `percept` using the `update_state` method. It then formulates a `goal` with the help of `formulate_goal` method and a `problem` using the `formulate_problem` method and returns a sequence of actions to solve it (using the `search` method).\n", "\n", From e245a64e51179d9b1c6883dcbaf58a7be094bd3a Mon Sep 17 00:00:00 2001 From: Aman Deep Singh Date: Thu, 15 Mar 2018 05:10:06 +0530 Subject: [PATCH 007/200] Added pl-fc-entails section (#818) * Added pl-fc-entails section * Updated README.md * Updated filename * Added tests for pl-fc-entails * Review fixes --- logic.ipynb | 849 ++++++++++++++++++++++++++++++++++++++++---- tests/test_logic.py | 8 + 2 files changed, 792 insertions(+), 65 deletions(-) diff --git a/logic.ipynb b/logic.ipynb index 0cd6cbc1f..92b8f51ed 100644 --- a/logic.ipynb +++ b/logic.ipynb @@ -946,7 +946,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -955,7 +955,7 @@ "(True, False)" ] }, - "execution_count": 22, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -973,7 +973,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -982,7 +982,7 @@ "(False, False)" ] }, - "execution_count": 23, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -1438,55 +1438,520 @@ "\n" ], "text/plain": [ - "" + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(pl_resolution)" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(True, False)" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pl_resolution(wumpus_kb, ~P11), pl_resolution(wumpus_kb, P11)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(False, False)" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pl_resolution(wumpus_kb, ~P22), pl_resolution(wumpus_kb, P22)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Forward and backward chaining\n", + "Previously, we said we will look at two algorithms to check if a sentence is entailed by the `KB`, \n", + "but here's a third one. \n", + "The difference here is that our goal now is to determine if a knowledge base of definite clauses entails a single proposition symbol *q* - the query.\n", + "There is a catch however, the knowledge base can only contain **Horn clauses**.\n", + "
\n", + "#### Horn Clauses\n", + "Horn clauses can be defined as a *disjunction* of *literals* with **at most** one positive literal. \n", + "
\n", + "A Horn clause with exactly one positive literal is called a *definite clause*.\n", + "
\n", + "A Horn clause might look like \n", + "
\n", + "$\\neg a\\lor\\neg b\\lor\\neg c\\lor\\neg d... \\lor z$\n", + "
\n", + "This, coincidentally, is also a definite clause.\n", + "
\n", + "Using De Morgan's laws, the example above can be simplified to \n", + "
\n", + "$a\\land b\\land c\\land d ... \\implies z$\n", + "
\n", + "This seems like a logical representation of how humans process known data and facts. \n", + "Assuming percepts `a`, `b`, `c`, `d` ... to be true simultaneously, we can infer `z` to also be true at that point in time. \n", + "There are some interesting aspects of Horn clauses that make algorithmic inference or *resolution* easier.\n", + "- Definite clauses can be written as implications:\n", + "
\n", + "The most important simplification a definite clause provides is that it can be written as an implication.\n", + "The premise (or the knowledge that leads to the implication) is a conjunction of positive literals.\n", + "The conclusion (the implied statement) is also a positive literal.\n", + "The sentence thus becomes easier to understand.\n", + "The premise and the conclusion are conventionally called the *body* and the *head* respectively.\n", + "A single positive literal is called a *fact*.\n", + "- Forward chaining and backward chaining can be used for inference from Horn clauses:\n", + "
\n", + "Forward chaining is semantically identical to `AND-OR-Graph-Search` from the chapter on search algorithms.\n", + "Implementational details will be explained shortly.\n", + "- Deciding entailment with Horn clauses is linear in size of the knowledge base:\n", + "
\n", + "Surprisingly, the forward and backward chaining algorithms traverse each element of the knowledge base at most once, greatly simplifying the problem.\n", + "
\n", + "
\n", + "The function `pl_fc_entails` implements forward chaining to see if a knowledge base `KB` entails a symbol `q`.\n", + "
\n", + "Before we proceed further, note that `pl_fc_entails` doesn't use an ordinary `KB` instance. \n", + "The knowledge base here is an instance of the `PropDefiniteKB` class, derived from the `PropKB` class, \n", + "but modified to store definite clauses.\n", + "
\n", + "The main point of difference arises in the inclusion of a helper method to `PropDefiniteKB` that returns a list of clauses in KB that have a given symbol `p` in their premise." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
    def clauses_with_premise(self, p):\n",
+       "        """Return a list of the clauses in KB that have p in their premise.\n",
+       "        This could be cached away for O(1) speed, but we'll recompute it."""\n",
+       "        return [c for c in self.clauses\n",
+       "                if c.op == '==>' and p in conjuncts(c.args[0])]\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(PropDefiniteKB.clauses_with_premise)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now have a look at the `pl_fc_entails` algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def pl_fc_entails(KB, q):\n",
+       "    """Use forward chaining to see if a PropDefiniteKB entails symbol q.\n",
+       "    [Figure 7.15]\n",
+       "    >>> pl_fc_entails(horn_clauses_KB, expr('Q'))\n",
+       "    True\n",
+       "    """\n",
+       "    count = {c: len(conjuncts(c.args[0]))\n",
+       "             for c in KB.clauses\n",
+       "             if c.op == '==>'}\n",
+       "    inferred = defaultdict(bool)\n",
+       "    agenda = [s for s in KB.clauses if is_prop_symbol(s.op)]\n",
+       "    while agenda:\n",
+       "        p = agenda.pop()\n",
+       "        if p == q:\n",
+       "            return True\n",
+       "        if not inferred[p]:\n",
+       "            inferred[p] = True\n",
+       "            for c in KB.clauses_with_premise(p):\n",
+       "                count[c] -= 1\n",
+       "                if count[c] == 0:\n",
+       "                    agenda.append(c.args[1])\n",
+       "    return False\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "psource(pl_fc_entails)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The function accepts a knowledge base `KB` (an instance of `PropDefiniteKB`) and a query `q` as inputs.\n", + "
\n", + "
\n", + "`count` initially stores the number of symbols in the premise of each sentence in the knowledge base.\n", + "
\n", + "The `conjuncts` helper function separates a given sentence at conjunctions.\n", + "
\n", + "`inferred` is initialized as a *boolean* defaultdict. \n", + "This will be used later to check if we have inferred all premises of each clause of the agenda.\n", + "
\n", + "`agenda` initially stores a list of clauses that the knowledge base knows to be true.\n", + "The `is_prop_symbol` helper function checks if the given symbol is a valid propositional logic symbol.\n", + "
\n", + "
\n", + "We now iterate through `agenda`, popping a symbol `p` on each iteration.\n", + "If the query `q` is the same as `p`, we know that entailment holds.\n", + "
\n", + "The agenda is processed, reducing `count` by one for each implication with a premise `p`.\n", + "A conclusion is added to the agenda when `count` reaches zero. This means we know all the premises of that particular implication to be true.\n", + "
\n", + "`clauses_with_premise` is a helpful method of the `PropKB` class.\n", + "It returns a list of clauses in the knowledge base that have `p` in their premise.\n", + "
\n", + "
\n", + "Now that we have an idea of how this function works, let's see a few examples of its usage, but we first need to define our knowledge base. We assume we know the following clauses to be true." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "clauses = ['(B & F)==>E', \n", + " '(A & E & F)==>G', \n", + " '(B & C)==>F', \n", + " '(A & B)==>D', \n", + " '(E & F)==>H', \n", + " '(H & I)==>J',\n", + " 'A', \n", + " 'B', \n", + " 'C']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now `tell` this information to our knowledge base." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "definite_clauses_KB = PropDefiniteKB()\n", + "for clause in clauses:\n", + " definite_clauses_KB.tell(expr(clause))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now check if our knowledge base entails the following queries." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" ] }, + "execution_count": 44, "metadata": {}, - "output_type": "display_data" + "output_type": "execute_result" } ], "source": [ - "psource(pl_resolution)" + "pl_fc_entails(definite_clauses_KB, expr('G'))" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 45, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(True, False)" + "True" ] }, - "execution_count": 25, + "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "pl_resolution(wumpus_kb, ~P11), pl_resolution(wumpus_kb, P11)" + "pl_fc_entails(definite_clauses_KB, expr('H'))" ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 46, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(False, False)" + "False" ] }, - "execution_count": 26, + "execution_count": 46, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "pl_resolution(wumpus_kb, ~P22), pl_resolution(wumpus_kb, P22)" + "pl_fc_entails(definite_clauses_KB, expr('I'))" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pl_fc_entails(definite_clauses_KB, expr('J'))" ] }, { @@ -2357,7 +2822,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 48, "metadata": { "collapsed": true }, @@ -2386,7 +2851,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 49, "metadata": { "collapsed": true }, @@ -2407,7 +2872,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 50, "metadata": { "collapsed": true }, @@ -2428,7 +2893,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 51, "metadata": { "collapsed": true }, @@ -2452,7 +2917,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 52, "metadata": { "collapsed": true }, @@ -2473,7 +2938,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 53, "metadata": { "collapsed": true }, @@ -2493,7 +2958,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 54, "metadata": { "collapsed": true }, @@ -2512,7 +2977,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 55, "metadata": { "collapsed": true }, @@ -2539,7 +3004,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 56, "metadata": {}, "outputs": [ { @@ -2548,7 +3013,7 @@ "{x: 3}" ] }, - "execution_count": 35, + "execution_count": 56, "metadata": {}, "output_type": "execute_result" } @@ -2559,7 +3024,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 57, "metadata": {}, "outputs": [ { @@ -2568,7 +3033,7 @@ "{x: B}" ] }, - "execution_count": 36, + "execution_count": 57, "metadata": {}, "output_type": "execute_result" } @@ -2579,7 +3044,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 58, "metadata": {}, "outputs": [ { @@ -2588,7 +3053,7 @@ "{x: Bella, y: Dobby}" ] }, - "execution_count": 37, + "execution_count": 58, "metadata": {}, "output_type": "execute_result" } @@ -2606,7 +3071,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 59, "metadata": {}, "outputs": [ { @@ -2630,7 +3095,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 60, "metadata": {}, "outputs": [ { @@ -2657,13 +3122,145 @@ }, { "cell_type": "code", - "execution_count": 40, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 61, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def fol_fc_ask(KB, alpha):\n",
+       "    """A simple forward-chaining algorithm. [Figure 9.3]"""\n",
+       "    # TODO: Improve efficiency\n",
+       "    kb_consts = list({c for clause in KB.clauses for c in constant_symbols(clause)})\n",
+       "    def enum_subst(p):\n",
+       "        query_vars = list({v for clause in p for v in variables(clause)})\n",
+       "        for assignment_list in itertools.product(kb_consts, repeat=len(query_vars)):\n",
+       "            theta = {x: y for x, y in zip(query_vars, assignment_list)}\n",
+       "            yield theta\n",
+       "\n",
+       "    # check if we can answer without new inferences\n",
+       "    for q in KB.clauses:\n",
+       "        phi = unify(q, alpha, {})\n",
+       "        if phi is not None:\n",
+       "            yield phi\n",
+       "\n",
+       "    while True:\n",
+       "        new = []\n",
+       "        for rule in KB.clauses:\n",
+       "            p, q = parse_definite_clause(rule)\n",
+       "            for theta in enum_subst(p):\n",
+       "                if set(subst(theta, p)).issubset(set(KB.clauses)):\n",
+       "                    q_ = subst(theta, q)\n",
+       "                    if all([unify(x, q_, {}) is None for x in KB.clauses + new]):\n",
+       "                        new.append(q_)\n",
+       "                        phi = unify(q_, alpha, {})\n",
+       "                        if phi is not None:\n",
+       "                            yield phi\n",
+       "        if not new:\n",
+       "            break\n",
+       "        for clause in new:\n",
+       "            KB.tell(clause)\n",
+       "    return None\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "%psource fol_fc_ask" + "psource(fol_fc_ask)" ] }, { @@ -2675,7 +3272,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 62, "metadata": {}, "outputs": [ { @@ -2700,7 +3297,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 63, "metadata": {}, "outputs": [ { @@ -2742,7 +3339,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 64, "metadata": { "collapsed": true }, @@ -2761,7 +3358,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 65, "metadata": { "collapsed": true }, @@ -2779,7 +3376,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 66, "metadata": { "collapsed": true }, @@ -2791,7 +3388,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 67, "metadata": {}, "outputs": [ { @@ -2800,7 +3397,7 @@ "{v_5: x, x: Nono}" ] }, - "execution_count": 46, + "execution_count": 67, "metadata": {}, "output_type": "execute_result" } @@ -2827,7 +3424,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 68, "metadata": {}, "outputs": [ { @@ -2836,7 +3433,7 @@ "(P ==> ~Q)" ] }, - "execution_count": 47, + "execution_count": 68, "metadata": {}, "output_type": "execute_result" } @@ -2854,7 +3451,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 69, "metadata": {}, "outputs": [ { @@ -2863,7 +3460,7 @@ "(P ==> ~Q)" ] }, - "execution_count": 48, + "execution_count": 69, "metadata": {}, "output_type": "execute_result" } @@ -2881,7 +3478,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": 70, "metadata": {}, "outputs": [ { @@ -2890,7 +3487,7 @@ "PartialExpr('==>', P)" ] }, - "execution_count": 49, + "execution_count": 70, "metadata": {}, "output_type": "execute_result" } @@ -2910,7 +3507,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 71, "metadata": {}, "outputs": [ { @@ -2919,7 +3516,7 @@ "(P ==> ~Q)" ] }, - "execution_count": 50, + "execution_count": 71, "metadata": {}, "output_type": "execute_result" } @@ -2949,7 +3546,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 72, "metadata": {}, "outputs": [ { @@ -2958,7 +3555,7 @@ "(~(P & Q) ==> (~P | ~Q))" ] }, - "execution_count": 51, + "execution_count": 72, "metadata": {}, "output_type": "execute_result" } @@ -2976,7 +3573,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 73, "metadata": {}, "outputs": [ { @@ -2985,7 +3582,7 @@ "(~(P & Q) ==> (~P | ~Q))" ] }, - "execution_count": 52, + "execution_count": 73, "metadata": {}, "output_type": "execute_result" } @@ -3004,7 +3601,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 74, "metadata": {}, "outputs": [ { @@ -3013,7 +3610,7 @@ "(((P & Q) ==> P) | Q)" ] }, - "execution_count": 53, + "execution_count": 74, "metadata": {}, "output_type": "execute_result" } @@ -3031,7 +3628,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 75, "metadata": {}, "outputs": [ { @@ -3040,7 +3637,7 @@ "((P & Q) ==> (P | Q))" ] }, - "execution_count": 54, + "execution_count": 75, "metadata": {}, "output_type": "execute_result" } @@ -3058,11 +3655,133 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 76, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "
\n", + "\n", + "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "from notebook import Canvas_fol_bc_ask\n", "canvas_bc_ask = Canvas_fol_bc_ask('canvas_bc_ask', crime_kb, expr('Criminal(x)'))" diff --git a/tests/test_logic.py b/tests/test_logic.py index 86bcc9ed6..6da2eb320 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -2,6 +2,10 @@ from logic import * from utils import expr_handle_infix_ops, count, Symbol +definite_clauses_KB = PropDefiniteKB() +for clause in ['(B & F)==>E', '(A & E & F)==>G', '(B & C)==>F', '(A & B)==>D', '(E & F)==>H', '(H & I)==>J', 'A', 'B', 'C']: + definite_clauses_KB.tell(expr(clause)) + def test_is_symbol(): assert is_symbol('x') @@ -154,6 +158,10 @@ def test_unify(): def test_pl_fc_entails(): assert pl_fc_entails(horn_clauses_KB, expr('Q')) + assert pl_fc_entails(definite_clauses_KB, expr('G')) + assert pl_fc_entails(definite_clauses_KB, expr('H')) + assert not pl_fc_entails(definite_clauses_KB, expr('I')) + assert not pl_fc_entails(definite_clauses_KB, expr('J')) assert not pl_fc_entails(horn_clauses_KB, expr('SomethingSilly')) From 49adcdb91636e0c5e126f8259fa01d2ffc67c0ef Mon Sep 17 00:00:00 2001 From: Kunwar Raj Singh Date: Thu, 15 Mar 2018 05:42:57 +0530 Subject: [PATCH 008/200] Implemented HybridWumpusAgent (#842) * Added WumpusKB for use in HybridWumpusAgent * Implemented HybridWumpusAgent added WumpusPosition helping class. --- logic.py | 307 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 306 insertions(+), 1 deletion(-) diff --git a/logic.py b/logic.py index 129d281cf..130718faa 100644 --- a/logic.py +++ b/logic.py @@ -690,16 +690,321 @@ def sat_count(sym): # ______________________________________________________________________________ +class WumpusKB(PropKB): + """ + Create a Knowledge Base that contains the atemporal "Wumpus physics" and temporal rules with time zero. + """ + def __init__(self,dimrow): + super().__init__() + self.dimrow = dimrow + self.tell('( NOT W1s1 )') + self.tell('( NOT P1s1 )') + for i in range(1, dimrow+1): + for j in range(1, dimrow+1): + bracket = 0 + sentence_b_str = "( B" + i + "s" + j + " <=> " + sentence_s_str = "( S" + i + "s" + j + " <=> " + if i > 1: + sentence_b_str += "( P" + (i-1) + "s" + j + " OR " + sentence_s_str += "( W" + (i-1) + "s" + j + " OR " + bracket += 1 + + if i < dimRow: + sentence_b_str += "( P" + (i+1) + "s" + j + " OR " + sentence_s_str += "( W" + (i+1) + "s" + j + " OR " + bracket += 1 + + if j > 1: + if j == dimRow: + sentence_b_str += "P" + i + "s" + (j-1) + " " + sentence_s_str += "W "+ i + "s" + (j-1) + " " + else: + sentence_b_str += "( P" + i + "s" + (j-1) + " OR " + sentence_s_str += "( W" + i + "s" + (j-1) + " OR " + bracket += 1 + + if j < dimRow: + sentence_b_str += "P" + i + "s" + (j+1) + " " + sentence_s_str += "W" + i + "s" + (j+1) + " " + + + for _ in range(bracket): + sentence_b_str += ") " + sentence_s_str += ") " + + sentence_b_str += ") " + sentence_s_str += ") " + + self.tell(sentence_b_str) + self.tell(sentence_s_str) + + + ## Rule that describes existence of at least one Wumpus + sentence_w_str = "" + for i in range(1, dimrow+1): + for j in range(1, dimrow+1): + if (i == dimrow) and (j == dimrow): + sentence_w_str += " W" + dimRow + "s" + dimrow + " " + else: + sentence_w_str += "( W" + i + "s" + j + " OR " + for _ in range(dimrow**2): + sentence_w_str += ") " + self.tell(sentence_w_str) + + + ## Rule that describes existence of at most one Wumpus + for i in range(1, dimrow+1): + for j in range(1, dimrow+1): + for u in range(1, dimrow+1): + for v in range(1, dimrow+1): + if i!=u or j!=v: + self.tell("( ( NOT W" + i + "s" + j + " ) OR ( NOT W" + u + "s" + v + " ) )") + + ## Temporal rules at time zero + self.tell("L1s1s0") + for i in range(1, dimrow+1): + for j in range(1, dimrow + 1): + self.tell("( L" + i + "s" + j + "s0 => ( Breeze0 <=> B" + i + "s" + j + " ) )") + self.tell("( L" + i + "s" + j + "s0 => ( Stench0 <=> S" + i + "s" + j + " ) )") + if i != 1 or j != 1: + self.tell("( NOT L" + i + "s" + j + "s" + "0 )") + self.tell("WumpusAlive0") + self.tell("HaveArrow0") + self.tell("FacingEast0") + self.tell("( NOT FacingWest0 )") + self.tell("( NOT FacingNorth0 )") + self.tell("( NOT FacingSouth0 )") + + + def make_action_sentence(self, action, time): + self.tell(action + time) + + + def make_percept_sentence(self, percept, time): + self.tell(percept + time) + + def add_temporal_sentences(self, time): + if time == 0: + return + t = time - 1 + + ## current location rules (L2s2s3 represent tile 2,2 at time 3) + ## ex.: ( L2s2s3 <=> ( ( L2s2s2 AND ( ( NOT Forward2 ) OR Bump3 ) ) + ## OR ( ( L1s2s2 AND ( FacingEast2 AND Forward2 ) ) OR ( L2s1s2 AND ( FacingNorth2 AND Forward2 ) ) ) + for i in range(1, self.dimrow+1): + for j in range(1, self.dimrow+1): + self.tell("( L" + i + "s" + j + "s" + time + " => ( Breeze" + time + " <=> B" + i + "s" + j + " ) )") + self.tell("( L" + i + "s" + j + "s" + time + " => ( Stench" + time + " <=> S" + i + "s" + j + " ) )") + s = "( L" + i + "s" + j + "s" + time + " <=> ( ( L" + i + "s" + j + "s" + t + " AND ( ( NOT Forward"\ + + t + " ) OR Bump" + time + " ) )" + + count = 2 + if i != 1: + s += " OR ( ( L" + (i - 1) + "s" + j + "s" + t + " AND ( FacingEast" + t + " AND Forward" + t\ + + " ) )" + count += 1 + if i != self.dimrow: + s += " OR ( ( L" + (i + 1) + "s" + j + "s" + t + " AND ( FacingWest" + t + " AND Forward" + t\ + + " ) )" + count += 1 + if j != 1: + if j == self.dimrow: + s += " OR ( L" + i + "s" + (j - 1) + "s" + t + " AND ( FacingNorth" + t + " AND Forward" + t\ + + " ) )" + else: + s += " OR ( ( L" + i + "s" + (j - 1) + "s" + t + " AND ( FacingNorth" + t + " AND Forward" \ + + t + " ) )" + count += 1 + if j != self.dimrow: + s += " OR ( L" + i + "s" + (j + 1) + "s" + t + " AND ( FacingSouth" + t + " AND Forward" + t\ + + " ) )" + + for _ in range(count): + s += " )" + + ## add sentence about location i,j + self.tell(s) + + ## add sentence about safety of location i,j + self.tell("( OK" + i + "s" + j + "s" + time + " <=> ( ( NOT P" + i + "s" + j + " ) AND ( NOT ( W" + i\ + + "s" + j + " AND WumpusAlive" + time + " ) ) ) )") + + ## Rules about current orientation + ## ex.: ( FacingEast3 <=> ( ( FacingNorth2 AND TurnRight2 ) OR ( ( FacingSouth2 AND TurnLeft2 ) + ## OR ( FacingEast2 AND ( ( NOT TurnRight2 ) AND ( NOT TurnLeft2 ) ) ) ) ) ) + a = "( FacingNorth" + t + " AND TurnRight" + t + " )" + b = "( FacingSouth" + t + " AND TurnLeft" + t + " )" + c = "( FacingEast" + t + " AND ( ( NOT TurnRight" + t + " ) AND ( NOT TurnLeft" + t + " ) ) )" + s = "( FacingEast" + (t + 1) + " <=> ( " + a + " OR ( " + b + " OR " + c + " ) ) )" + this.tell(s) + + a = "( FacingNorth" + t + " AND TurnLeft" + t + " )" + b = "( FacingSouth" + t + " AND TurnRight" + t + " )" + c = "( FacingWest" + t + " AND ( ( NOT TurnRight" + t + " ) AND ( NOT TurnLeft" + t + " ) ) )" + s = "( FacingWest" + (t + 1) + " <=> ( " + a + " OR ( " + b + " OR " + c + " ) ) )" + this.tell(s) + + a = "( FacingEast" + t + " AND TurnLeft" + t + " )" + b = "( FacingWest" + t + " AND TurnRight" + t + " )" + c = "( FacingNorth" + t + " AND ( ( NOT TurnRight" + t + " ) AND ( NOT TurnLeft" + t + " ) ) )" + s = "( FacingNorth" + (t + 1) + " <=> ( " + a + " OR ( " + b + " OR " + c + " ) ) )" + this.tell(s) + + a = "( FacingWest" + t + " AND TurnLeft" + t + " )" + b = "( FacingEast" + t + " AND TurnRight" + t + " )" + c = "( FacingSouth" + t + " AND ( ( NOT TurnRight" + t + " ) AND ( NOT TurnLeft" + t + " ) ) )" + s = "( FacingSouth" + (t + 1) + " <=> ( " + a + " OR ( " + b + " OR " + c + " ) ) )" + this.tell(s) + + ## Rules about last action + self.tell("( Forward" + t + " <=> ( NOT TurnRight" + t + " ) )") + self.tell("( Forward" + t + " <=> ( NOT TurnLeft" + t + " ) )") + + ##Rule about the arrow + self.tell("( HaveArrow" + time + " <=> ( HaveArrow" + (time - 1) + " AND ( NOT Shot" + (time - 1) + " ) ) )") + + ##Rule about Wumpus (dead or alive) + self.tell("( WumpusAlive" + time + " <=> ( WumpusAlive" + (time - 1) + " AND ( NOT Scream" + time + " ) ) )") + + +# ______________________________________________________________________________ + + +class WumpusPosition(): + def __init__(self, X, Y, orientation): + self.X = X + self.Y = Y + self.orientation = orientation + + + def get_location(self): + return self.X, self.Y + + def get_orientation(self): + return self.orientation + + def equals(self, wumpus_position): + if wumpus_position.get_location() == self.get_location() and \ + wumpus_position.get_orientation()==self.get_orientation(): + return True + else: + return False + +# ______________________________________________________________________________ + + class HybridWumpusAgent(agents.Agent): """An agent for the wumpus world that does logical inference. [Figure 7.20]""" def __init__(self): - raise NotImplementedError + super().__init__() + self.dimrow = 3 + self.kb = WumpusKB(self.dimrow) + self.t = 0 + self.plan = list() + self.current_position = WumpusPosition(1, 1, 'UP') + + + def execute(self, percept): + self.kb.make_percept_sentence(percept, self.t) + self.kb.add_temporal_sentences(self.t) + + temp = list() + + for i in range(1, self.dimrow+1): + for j in range(1, self.dimrow+1): + if self.kb.ask_with_dpll('L' + i + 's' + j + 's' + self.t): + temp.append(i) + temp.append(j) + + if self.kb.ask_with_dpll('FacingNorth' + self.t): + self.current_position = WumpusPosition(temp[0], temp[1], 'UP') + elif self.kb.ask_with_dpll('FacingSouth' + self.t): + self.current_position = WumpusPosition(temp[0], temp[1], 'DOWN') + elif self.kb.ask_with_dpll('FacingWest' + self.t): + self.current_position = WumpusPosition(temp[0], temp[1], 'LEFT') + elif self.kb.ask_with_dpll('FacingEast' + self.t): + self.current_position = WumpusPosition(temp[0], temp[1], 'RIGHT') + + safe_points = list() + for i in range(1, self.dimrow+1): + for j in range(1, self.dimrow+1): + if self.kb.ask_with_dpll('OK' + i + 's' + j + 's' + self.t): + safe_points.append([i, j]) + + if self.kb.ask_with_dpll('Glitter' + self.t): + goals = list() + goals.append([1, 1]) + self.plan.append('Grab') + actions = plan_route(self.current_position,goals,safe_points) + for action in actions: + self.plan.append(action) + self.plan.append('Climb') + + if len(self.plan) == 0: + unvisited = list() + for i in range(1, self.dimrow+1): + for j in range(1, self.dimrow+1): + for k in range(1, self.dimrow+1): + if self.kb.ask_with_dpll("L" + i + "s" + j + "s" + k): + unvisited.append([i, j]) + unvisited_and_safe = list() + for u in unvisited: + for s in safe_points: + if u not in unvisited_and_safe and s == u: + unvisited_and_safe.append(u) + + temp = plan_route(self.current_position,unvisited_and_safe,safe_points) + for t in temp: + self.plan.append(t) + + if len(self.plan) == 0 and self.kb.ask_with_dpll('HaveArrow' + self.t): + possible_wumpus = list() + for i in range(1, self.dimrow+1): + for j in range(1, self.dimrow+1): + if not self.kb.ask_with_dpll('W' + i + 's' + j): + possible_wumpus.append([i, j]) + + temp = plan_shot(self.current_position, possible_wumpus, safe_points) + for t in temp: + self.plan.append(t) + + if len(self.plan) == 0: + not_unsafe = list() + for i in range(1, self.dimrow+1): + for j in range(1, self.dimrow+1): + if not self.kb.ask_with_dpll('OK' + i + 's' + j + 's' + self.t): + not_unsafe.append([i, j]) + temp = plan_route(self.current_position, not_unsafe, safe_points) + for t in temp: + self.plan.append(t) + + if len(self.plan) == 0: + start = list() + start.append([1, 1]) + temp = plan_route(self.current_position, start, safe_points) + for t in temp: + self.plan.append(t) + self.plan.append('Climb') + + + + action = self.plan[1:] + + self.kb.make_action_sentence(action, self.t) + self.t += 1 + + return action def plan_route(current, goals, allowed): raise NotImplementedError + +def plan_shot(current, goals, allowed): + raise NotImplementedError + + # ______________________________________________________________________________ From c13408dbb36671172fe1c2d078bf73a907326cbd Mon Sep 17 00:00:00 2001 From: Dimkoim Date: Thu, 15 Mar 2018 01:19:25 +0100 Subject: [PATCH 009/200] Forward-Backward examples added to the probability.ipynb. Fixes issue #813 (#827) * Forward-Backward examples added to the ipynb. Fixes issue #813 * Forward-Backward examples added to the probability.ipynb. Fixes issue #813 * Convert Latex syntax to Markdown except from the equations with subscript characters --- probability.ipynb | 401 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 369 insertions(+), 32 deletions(-) diff --git a/probability.ipynb b/probability.ipynb index 2fd1c9dae..365039874 100644 --- a/probability.ipynb +++ b/probability.ipynb @@ -11,21 +11,19 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from probability import *\n", - "from notebook import psource" + "from notebook import *" ] }, { "cell_type": "markdown", - "metadata": { - "collapsed": true - }, + "metadata": {}, "source": [ "## Probability Distribution\n", "\n", @@ -34,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "metadata": { "collapsed": true }, @@ -45,7 +43,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -54,7 +52,7 @@ "0.75" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -255,9 +253,7 @@ }, { "cell_type": "markdown", - "metadata": { - "collapsed": true - }, + "metadata": {}, "source": [ "_A probability model is completely determined by the joint distribution for all of the random variables._ (**Section 13.3**) The probability module implements these as the class **JointProbDist** which inherits from the **ProbDist** class. This class specifies a discrete probability distribute over a set of variables. " ] @@ -512,9 +508,124 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + "\n", + "\n", + "

\n", + "\n", + "
def enumerate_joint_ask(X, e, P):\n",
+       "    """Return a probability distribution over the values of the variable X,\n",
+       "    given the {var:val} observations e, in the JointProbDist P. [Section 13.3]\n",
+       "    >>> P = JointProbDist(['X', 'Y'])\n",
+       "    >>> P[0,0] = 0.25; P[0,1] = 0.5; P[1,1] = P[2,1] = 0.125\n",
+       "    >>> enumerate_joint_ask('X', dict(Y=1), P).show_approx()\n",
+       "    '0: 0.667, 1: 0.167, 2: 0.167'\n",
+       "    """\n",
+       "    assert X not in e, "Query variable must be distinct from evidence"\n",
+       "    Q = ProbDist(X)  # probability distribution for X, initially empty\n",
+       "    Y = [v for v in P.variables if v != X and v not in e]  # hidden variables.\n",
+       "    for xi in P.values(X):\n",
+       "        Q[xi] = enumerate_joint(Y, extend(e, X, xi), P)\n",
+       "    return Q.normalize()\n",
+       "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "psource(enumerate_joint_ask)" ] @@ -792,7 +903,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": {}, "outputs": [], "source": [ @@ -1178,7 +1289,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "metadata": { "collapsed": true }, @@ -1418,21 +1529,8 @@ ] }, { - "cell_type": "code", - "execution_count": 45, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'False: 0.184, True: 0.816'" - ] - }, - "execution_count": 45, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "likelihood_weighting('Cloudy', dict(Rain=True), sprinkler, 200).show_approx()" ] @@ -1450,7 +1548,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": { "collapsed": true }, @@ -1485,6 +1583,245 @@ "source": [ "gibbs_ask('Cloudy', dict(Rain=True), sprinkler, 200).show_approx()" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inference in Temporal Models" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before we start, it will be helpful to understand the structure of a temporal model. We will use the example of the book with the guard and the umbrella. In this example, the state $\\textbf{X}$ is whether it is a rainy day (`X = True`) or not (`X = False`) at Day $\\textbf{t}$. In the sensor or observation model, the observation or evidence $\\textbf{U}$ is whether the professor holds an umbrella (`U = True`) or not (`U = False`) on **Day** $\\textbf{t}$. Based on that, the transition model is \n", + "\n", + "| $X_{t-1}$ | $X_{t}$ | **P**$(X_{t}| X_{t-1})$| \n", + "| ------------- |------------- | ----------------------------------|\n", + "| ***${False}$*** | ***${False}$*** | 0.7 |\n", + "| ***${False}$*** | ***${True}$*** | 0.3 |\n", + "| ***${True}$*** | ***${False}$*** | 0.3 |\n", + "| ***${True}$*** | ***${True}$*** | 0.7 |\n", + "\n", + "And the the sensor model will be,\n", + "\n", + "| $X_{t}$ | $U_{t}$ | **P**$(U_{t}|X_{t})$| \n", + "| :-------------: |:-------------: | :------------------------:|\n", + "| ***${False}$*** | ***${True}$*** | 0.2 |\n", + "| ***${False}$*** | ***${False}$*** | 0.8 |\n", + "| ***${True}$*** | ***${True}$*** | 0.9 |\n", + "| ***${True}$*** | ***${False}$*** | 0.1 |\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the filtering task we are given evidence **U** in each time **t** and we want to compute the belief $B_{t}(x)= P(X_{t}|U_{1:t})$. \n", + "We can think of it as a three step process:\n", + "1. In every step we start with the current belief $P(X_{t}|e_{1:t})$\n", + "2. We update it for time\n", + "3. We update it for evidence\n", + "\n", + "The forward algorithm performs the step 2 and 3 at once. It updates, or better say reweights, the initial belief using the transition and the sensor model. Let's see the umbrella example. On **Day 0** no observation is available, and for that reason we will assume that we have equal possibilities to rain or not. In the **`HiddenMarkovModel`** class, the prior probabilities for **Day 0** are by default [0.5, 0.5]. " + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "%psource HiddenMarkovModel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We instantiate the object **`hmm`** of the class using a list of lists for both the transition and the sensor model." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "umbrella_transition_model = [[0.7, 0.3], [0.3, 0.7]]\n", + "umbrella_sensor_model = [[0.9, 0.2], [0.1, 0.8]]\n", + "hmm = HiddenMarkovModel(umbrella_transition_model, umbrella_sensor_model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The **`sensor_dist()`** method returns a list with the conditional probabilities of the sensor model." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[0.9, 0.2]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "hmm.sensor_dist(ev=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The observation update is calculated with the **`forward()`** function. Basically, we update our belief using the observation model. The function returns a list with the probabilities of **raining or not** on **Day 1**." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "psource(forward)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The probability of raining on day 1 is 0.82\n" + ] + } + ], + "source": [ + "belief_day_1 = forward(hmm, umbrella_prior, ev=True)\n", + "print ('The probability of raining on day 1 is {:.2f}'.format(belief_day_1[0]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In **Day 2** our initial belief is the updated belief of **Day 1**. Again using the **`forward()`** function we can compute the probability of raining in **Day 2**" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The probability of raining in day 2 is 0.88\n" + ] + } + ], + "source": [ + "belief_day_2 = forward(hmm, belief_day_1, ev=True)\n", + "print ('The probability of raining in day 2 is {:.2f}'.format(belief_day_2[0]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the smoothing part we are interested in computing the distribution over past states given evidence up to the present. Assume that we want to compute the distribution for the time **k**, for $0\\leq k Date: Wed, 14 Mar 2018 20:45:34 -0400 Subject: [PATCH 010/200] Add test for TableDrivenAgentProgram. (#749) Fixes #748. --- tests/test_agents.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_agents.py b/tests/test_agents.py index d5f63bc48..ded9b7d95 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -208,6 +208,20 @@ def test_compare_agents() : assert performance_ReflexVacummAgent <= performance_ModelBasedVacummAgent +def test_TableDrivenAgentProgram(): + table = {(('foo', 1),): 'action1', + (('foo', 2),): 'action2', + (('bar', 1),): 'action3', + (('bar', 2),): 'action1', + (('foo', 1), ('foo', 1),): 'action2', + (('foo', 1), ('foo', 2),): 'action3', + } + agent_program = TableDrivenAgentProgram(table) + assert agent_program(('foo', 1)) == 'action1' + assert agent_program(('foo', 2)) == 'action3' + assert agent_program(('invalid percept',)) == None + + def test_Agent(): def constant_prog(percept): return percept From 11cc2ccee345dc8ce5787bc4dcd303b259d81350 Mon Sep 17 00:00:00 2001 From: Aman Deep Singh Date: Thu, 15 Mar 2018 06:28:10 +0530 Subject: [PATCH 011/200] Refactored EightPuzzle class (#807) * Refactor EightPuzzle class * return instead of print * Added tests for EightPuzzle * Review fixes * Review fixes * Fixed tests * Update inverted commas in docstrings --- search.py | 125 +++++++++++++++++-------------------------- tests/test_search.py | 59 ++++++++++++++++++++ 2 files changed, 108 insertions(+), 76 deletions(-) diff --git a/search.py b/search.py index a80a48c8c..7480d28ca 100644 --- a/search.py +++ b/search.py @@ -411,102 +411,75 @@ def astar_search(problem, h=None): class EightPuzzle(Problem): - """The problem of sliding tiles numbered from 1 to 8 on a 3x3 board, + """ The problem of sliding tiles numbered from 1 to 8 on a 3x3 board, where one of the squares is a blank. A state is represented as a 3x3 list, - where element at index i,j represents the tile number (0 if it's an empty square).""" + where element at index i,j represents the tile number (0 if it's an empty square) """ - def __init__(self, initial, goal=None): - if goal: - self.goal = goal - else: - self.goal = [ [0,1,2], - [3,4,5], - [6,7,8] ] + def __init__(self, initial, goal=(1, 2, 3, 4, 5, 6, 7, 8, 0)): + """ Define goal state and initialize a problem """ + + self.goal = goal Problem.__init__(self, initial, goal) def find_blank_square(self, state): """Return the index of the blank square in a given state""" - for row in len(state): - for column in len(row): - if state[row][column] == 0: - index_blank_square = (row, column) - return index_blank_square + + return state.index(0) def actions(self, state): - """Return the actions that can be executed in the given state. + """ Return the actions that can be executed in the given state. The result would be a list, since there are only four possible actions - in any given state of the environment.""" - - possible_actions = list() + in any given state of the environment """ + + possible_actions = ['UP', 'DOWN', 'LEFT', 'RIGHT'] index_blank_square = self.find_blank_square(state) - if index_blank_square(0) == 0: - possible_actions += ['DOWN'] - elif index_blank_square(0) == 1: - possible_actions += ['UP', 'DOWN'] - elif index_blank_square(0) == 2: - possible_actions += ['UP'] - - if index_blank_square(1) == 0: - possible_actions += ['RIGHT'] - elif index_blank_square(1) == 1: - possible_actions += ['LEFT', 'RIGHT'] - elif index_blank_square(1) == 2: - possible_actions += ['LEFT'] + if index_blank_square % 3 == 0: + possible_actions.remove('LEFT') + if index_blank_square < 3: + possible_actions.remove('UP') + if index_blank_square % 3 == 2: + possible_actions.remove('RIGHT') + if index_blank_square > 5: + possible_actions.remove('DOWN') return possible_actions def result(self, state, action): - """Given state and action, return a new state that is the result of the action. - Action is assumed to be a valid action in the state.""" - - blank_square = self.find_blank_square(state) - new_state = [row[:] for row in state] - - if action=='UP': - new_state[blank_square(0)][blank_square(1)] = new_state[blank_square(0)-1][blank_square(1)] - new_state[blank_square(0)-1][blank_square(1)] = 0 - elif action=='LEFT': - new_state[blank_square(0)][blank_square(1)] = new_state[blank_square(0)][blank_square(1)-1] - new_state[blank_square(0)][blank_square(1)-1] = 0 - elif action=='DOWN': - new_state[blank_square(0)][blank_square(1)] = new_state[blank_square(0)+1][blank_square(1)] - new_state[blank_square(0)+1][blank_square(1)] = 0 - elif action=='RIGHT': - new_state[blank_square(0)][blank_square(1)] = new_state[blank_square(0)][blank_square(1)+1] - new_state[blank_square(0)][blank_square(1)+1] = 0 - else: - print("Invalid Action!") - return new_state + """ Given state and action, return a new state that is the result of the action. + Action is assumed to be a valid action in the state """ + + # blank is the index of the blank square + blank = self.find_blank_square(state) + new_state = list(state) + + delta = {'UP':-3, 'DOWN':3, 'LEFT':-1, 'RIGHT':1} + neighbor = blank + delta[action] + new_state[blank], new_state[neighbor] = new_state[neighbor], new_state[blank] + + return tuple(new_state) def goal_test(self, state): - """Given a state, return True if state is a goal state or False, otherwise""" - for row in len(state): - for column in len(row): - if state[row][col] != self.goal[row][column]: - return False - return True - - def checkSolvability(self, state): + """ Given a state, return True if state is a goal state or False, otherwise """ + + return state == self.goal + + def check_solvability(self, state): + """ Checks if the given state is solvable """ + inversion = 0 for i in range(len(state)): - for j in range(i, len(state)): - if (state[i] > state[j] and state[j] != 0): - inversion += 1 - check = True - if inversion%2 != 0: - check = False - print(check) + for j in range(i, len(state)): + if (state[i] > state[j] and state[j] != 0): + inversion += 1 + + return (inversion % 2 == 0) - def h(self, state): - """Return the heuristic value for a given state. Heuristic function used is - h(n) = number of misplaced tiles.""" - num_misplaced_tiles = 0 - for row in len(state): - for column in len(row): - if state[row][col] != self.goal[row][column]: - num_misplaced_tiles += 1 - return num_misplaced_tiles + def h(self, node): + """ Return the heuristic value for a given state. Default heuristic function used is + h(n) = number of misplaced tiles """ + + return sum(s != g for (s, g) in zip(node.state, self.goal)) # ______________________________________________________________________________ # Other search algorithms diff --git a/tests/test_search.py b/tests/test_search.py index 23f8b0f43..f35755315 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -5,6 +5,8 @@ romania_problem = GraphProblem('Arad', 'Bucharest', romania_map) vacumm_world = GraphProblemStochastic('State_1', ['State_7', 'State_8'], vacumm_world) LRTA_problem = OnlineSearchProblem('State_3', 'State_5', one_dim_state_space) +eight_puzzle = EightPuzzle((1, 2, 3, 4, 5, 7, 8, 6, 0)) +eight_puzzle2 = EightPuzzle((1, 0, 6, 8, 7, 5, 4, 2), (0, 1, 2, 3, 4, 5, 6, 7, 8)) def test_find_min_edge(): assert romania_problem.find_min_edge() == 70 @@ -64,6 +66,63 @@ def test_bidirectional_search(): def test_astar_search(): assert astar_search(romania_problem).solution() == ['Sibiu', 'Rimnicu', 'Pitesti', 'Bucharest'] + assert astar_search(eight_puzzle).solution() == ['LEFT', 'LEFT', 'UP', 'RIGHT', 'RIGHT', 'DOWN', 'LEFT', 'UP', 'LEFT', 'DOWN', 'RIGHT', 'RIGHT'] + assert astar_search(EightPuzzle((1, 2, 3, 4, 5, 6, 0, 7, 8))).solution() == ['RIGHT', 'RIGHT'] + + +def test_find_blank_square(): + assert eight_puzzle.find_blank_square((0, 1, 2, 3, 4, 5, 6, 7, 8)) == 0 + assert eight_puzzle.find_blank_square((6, 3, 5, 1, 8, 4, 2, 0, 7)) == 7 + assert eight_puzzle.find_blank_square((3, 4, 1, 7, 6, 0, 2, 8, 5)) == 5 + assert eight_puzzle.find_blank_square((1, 8, 4, 7, 2, 6, 3, 0, 5)) == 7 + assert eight_puzzle.find_blank_square((4, 8, 1, 6, 0, 2, 3, 5, 7)) == 4 + assert eight_puzzle.find_blank_square((1, 0, 6, 8, 7, 5, 4, 2, 3)) == 1 + assert eight_puzzle.find_blank_square((1, 2, 3, 4, 5, 6, 7, 8, 0)) == 8 + + +def test_actions(): + assert eight_puzzle.actions((0, 1, 2, 3, 4, 5, 6, 7, 8)) == ['DOWN', 'RIGHT'] + assert eight_puzzle.actions((6, 3, 5, 1, 8, 4, 2, 0, 7)) == ['UP', 'LEFT', 'RIGHT'] + assert eight_puzzle.actions((3, 4, 1, 7, 6, 0, 2, 8, 5)) == ['UP', 'DOWN', 'LEFT'] + assert eight_puzzle.actions((1, 8, 4, 7, 2, 6, 3, 0, 5)) == ['UP', 'LEFT', 'RIGHT'] + assert eight_puzzle.actions((4, 8, 1, 6, 0, 2, 3, 5, 7)) == ['UP', 'DOWN', 'LEFT', 'RIGHT'] + assert eight_puzzle.actions((1, 0, 6, 8, 7, 5, 4, 2, 3)) == ['DOWN', 'LEFT', 'RIGHT'] + assert eight_puzzle.actions((1, 2, 3, 4, 5, 6, 7, 8, 0)) == ['UP', 'LEFT'] + + +def test_result(): + assert eight_puzzle.result((0, 1, 2, 3, 4, 5, 6, 7, 8), 'DOWN') == (3, 1, 2, 0, 4, 5, 6, 7, 8) + assert eight_puzzle.result((6, 3, 5, 1, 8, 4, 2, 0, 7), 'LEFT') == (6, 3, 5, 1, 8, 4, 0, 2, 7) + assert eight_puzzle.result((3, 4, 1, 7, 6, 0, 2, 8, 5), 'UP') == (3, 4, 0, 7, 6, 1, 2, 8, 5) + assert eight_puzzle.result((1, 8, 4, 7, 2, 6, 3, 0, 5), 'RIGHT') == (1, 8, 4, 7, 2, 6, 3, 5, 0) + assert eight_puzzle.result((4, 8, 1, 6, 0, 2, 3, 5, 7), 'LEFT') == (4, 8, 1, 0, 6, 2, 3, 5, 7) + assert eight_puzzle.result((1, 0, 6, 8, 7, 5, 4, 2, 3), 'DOWN') == (1, 7, 6, 8, 0, 5, 4, 2, 3) + assert eight_puzzle.result((1, 2, 3, 4, 5, 6, 7, 8, 0), 'UP') == (1, 2, 3, 4, 5, 0, 7, 8, 6) + assert eight_puzzle.result((4, 8, 1, 6, 0, 2, 3, 5, 7), 'RIGHT') == (4, 8, 1, 6, 2, 0, 3, 5, 7) + + +def test_goal_test(): + assert eight_puzzle.goal_test((0, 1, 2, 3, 4, 5, 6, 7, 8)) == False + assert eight_puzzle.goal_test((6, 3, 5, 1, 8, 4, 2, 0, 7)) == False + assert eight_puzzle.goal_test((3, 4, 1, 7, 6, 0, 2, 8, 5)) == False + assert eight_puzzle.goal_test((1, 2, 3, 4, 5, 6, 7, 8, 0)) == True + assert eight_puzzle2.goal_test((4, 8, 1, 6, 0, 2, 3, 5, 7)) == False + assert eight_puzzle2.goal_test((3, 4, 1, 7, 6, 0, 2, 8, 5)) == False + assert eight_puzzle2.goal_test((1, 2, 3, 4, 5, 6, 7, 8, 0)) == False + assert eight_puzzle2.goal_test((0, 1, 2, 3, 4, 5, 6, 7, 8)) == True + + +def test_check_solvability(): + assert eight_puzzle.check_solvability((0, 1, 2, 3, 4, 5, 6, 7, 8)) == True + assert eight_puzzle.check_solvability((6, 3, 5, 1, 8, 4, 2, 0, 7)) == True + assert eight_puzzle.check_solvability((3, 4, 1, 7, 6, 0, 2, 8, 5)) == True + assert eight_puzzle.check_solvability((1, 8, 4, 7, 2, 6, 3, 0, 5)) == True + assert eight_puzzle.check_solvability((4, 8, 1, 6, 0, 2, 3, 5, 7)) == True + assert eight_puzzle.check_solvability((1, 0, 6, 8, 7, 5, 4, 2, 3)) == True + assert eight_puzzle.check_solvability((1, 2, 3, 4, 5, 6, 7, 8, 0)) == True + assert eight_puzzle.check_solvability((1, 2, 3, 4, 5, 6, 8, 7, 0)) == False + assert eight_puzzle.check_solvability((1, 0, 3, 2, 4, 5, 6, 7, 8)) == False + assert eight_puzzle.check_solvability((7, 0, 2, 8, 5, 3, 6, 4, 1)) == False def test_recursive_best_first_search(): From 651cf66bbb289a3dd1dbccf03e95e964af8aaaad Mon Sep 17 00:00:00 2001 From: Aman Deep Singh Date: Thu, 15 Mar 2018 13:11:28 +0530 Subject: [PATCH 012/200] Changed plotting function for NQueensCSP (#847) * Updated README.md * Added function to plot NQueensProblem * Added queen image * Changed plotting function for NQueensCSP * Replaced f'{}' with .format() notation * Added Pillow to travis.yml --- .travis.yml | 1 + README.md | 2 +- csp.ipynb | 61 +++++++++++---------------------------------- images/queen_s.png | Bin 0 -> 14407 bytes notebook.py | 30 +++++++++++++++++++++- 5 files changed, 45 insertions(+), 49 deletions(-) create mode 100644 images/queen_s.png diff --git a/.travis.yml b/.travis.yml index 600d6bd00..e374eff1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ install: - pip install matplotlib - pip install networkx - pip install ipywidgets + - pip install Pillow script: - py.test diff --git a/README.md b/README.md index 3ab5777c1..4b8b4528f 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 6 | CSP | `CSP` | [`csp.py`][csp] | Done | Included | | 6.3 | AC-3 | `AC3` | [`csp.py`][csp] | Done | | | 6.5 | Backtracking-Search | `backtracking_search` | [`csp.py`][csp] | Done | Included | -| 6.8 | Min-Conflicts | `min_conflicts` | [`csp.py`][csp] | Done | | +| 6.8 | Min-Conflicts | `min_conflicts` | [`csp.py`][csp] | Done | Included | | 6.11 | Tree-CSP-Solver | `tree_csp_solver` | [`csp.py`][csp] | Done | Included | | 7 | KB | `KB` | [`logic.py`][logic] | Done | Included | | 7.1 | KB-Agent | `KB_Agent` | [`logic.py`][logic] | Done | | diff --git a/csp.ipynb b/csp.ipynb index be3882387..af85b81d6 100644 --- a/csp.ipynb +++ b/csp.ipynb @@ -18,7 +18,8 @@ "outputs": [], "source": [ "from csp import *\n", - "from notebook import psource, pseudocode\n", + "from notebook import psource, pseudocode, plot_NQueens\n", + "%matplotlib inline\n", "\n", "# Hide warnings in the matplotlib sections\n", "import warnings\n", @@ -159,9 +160,9 @@ { "data": { "text/plain": [ - "(,\n", - " ,\n", - " )" + "(,\n", + " ,\n", + " )" ] }, "execution_count": 3, @@ -684,47 +685,20 @@ "metadata": {}, "source": [ "This is indeed a valid solution. \n", - "Let's write a helper function to visualize the solution space." + "
\n", + "`notebook.py` has a helper function to visualize the solution space." ] }, { "cell_type": "code", "execution_count": 9, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "%matplotlib inline\n", - "\n", - "def display_NQueensCSP(solution):\n", - " n = len(solution)\n", - " board = np.array([2 * int((i + j) % 2) for j in range(n) for i in range(n)]).reshape((n, n))\n", - " \n", - " for (k, v) in solution.items():\n", - " board[k][v] = 1\n", - " \n", - " fig = plt.figure(figsize=(7, 7))\n", - " ax = fig.add_subplot(111)\n", - " ax.set_title(f'{n} Queens')\n", - " plt.imshow(board, cmap='binary', interpolation='nearest')\n", - " ax.set_aspect('equal')\n", - " fig.tight_layout()\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 10, "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAeAAAAHwCAYAAAB+ArwOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4wLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvpW3flQAAFZFJREFUeJzt3HuspAd53/HfE6+52DFxG7bUFwpE\njSxR1AB7IEWuaIshsQMlVS+SaYNCVNVpGxLcRk1J/tmlSqU2f0SkokXZGAhJAItrRRGYECU0RW0M\nZ40pGEMFxhGLcbxu4hpwg7Hz9I8zbpdllzPbzOzjM+fzkY58Zuad9zzj18ff815mqrsDAJxb3zE9\nAADsRwIMAAMEGAAGCDAADBBgABggwAAwQIABYIAAA8AAAYZzoKqeWlXvr6o/qqq7q+p1VXXg2yx/\ncVW9frHsA1X1yar60XM5M7BeAgznxn9Ick+SS5I8M8lfS/JPT7dgVT0myW8leUqS5yX5riT/Iskv\nVNVPnZNpgbUTYDg3npbk7d39x919d5KbkvylMyz78iR/Icnf6+4vdPc3uvumJD+V5Oer6qIkqaqu\nqr/4yJOq6ler6udPuv2Sqrq1qu6rqv9aVX/5pMcurap3VdWJqvrCyWGvqiNV9faq+rWq+kpV3VZV\nWyc9/i+r6kuLxz5bVVet5l8R7C8CDOfGLyW5tqouqKrLklyTnQifzouSfKC7v3bK/e9KckGSv7Lb\nD6uqZyd5Y5IfT/LdSX45yXur6rFV9R1J/lOSTyS5LMlVSa6vqh88aRUvTXJjkouTvDfJ6xbrvSLJ\nK5M8p7svSvKDSe7cbR7gWwkwnBv/OTt7vPcnOZ5kO8l/PMOyT0zy5VPv7O6Hktyb5OASP+8fJfnl\n7r65ux/u7jcn+Xp24v2cJAe7+19194PdfUeSX0ly7UnP/0h3v7+7H07y60m+b3H/w0kem+TpVXV+\nd9/Z3Z9fYh7gFAIMa7bY4/xgkncnuTA7gf0zSf7tGZ5yb3bOFZ+6ngOL555Y4sc+JclPLw4/31dV\n9yV5cpJLF49despjP5fkSSc9/+6Tvn8gyeOq6kB3fy7J9UmOJLmnqm6sqkuXmAc4hQDD+v3Z7MTv\ndd399e7+n0nelOSHzrD8byW5pqouPOX+v5PkG0k+urj9QHYOST/iz5/0/ReT/Ovuvvikrwu6+22L\n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\nIMAAMECAAWCAAAPAAAEGgAECDAADBBgABggwAAwQYAAYIMAAMECAAWCAAAPAAAEGgAECDAADBBgA\nBggwAAwQYAAYIMAAMKC6e/UrrTqR5PdXvuIze2KSe8/hzzvXvL69zevb+zb9NXp9q/WU7j6420Jr\nCfC5VlXb3b01Pce6eH17m9e39236a/T6ZjgEDQADBBgABmxKgI9OD7BmXt/e5vXtfZv+Gr2+ARtx\nDhgA9ppN2QMGgD1FgAFgwJ4OcFVdXVWfrarPVdWrp+dZtap6Y1XdU1Wfmp5lHarqyVX1O1V1e1Xd\nVlWvmp5plarqcVX10ar6xOL1vWZ6pnWoqvOq6uNV9b7pWVatqu6sqk9W1a1VtT09z6pV1cVV9c6q\n+szi9/B50zOtUlVdsdh2j3zdX1XXT8/1iD17DriqzkvyP5K8KMnxJB9L8rLu/vToYCtUVc9P8tUk\nv9bdz5ieZ9Wq6pIkl3T3LVV1UZJjSf7WpmzDqqokF3b3V6vq/CQfSfKq7v694dFWqqr+eZKtJE/o\n7pdMz7NKVXVnkq3u3sgPqaiqNyf5L919Q1U9JskF3X3f9FzrsGjGl5J8f3efyw+KOqO9vAf83CSf\n6+47uvvBJDcm+eHhmVaqu383yR9Oz7Eu3f3l7r5l8f1Xktye5LLZqVand3x1cfP8xdfe/Iv3DKrq\n8iQvTnLD9Cycnap6QpLnJ3lDknT3g5sa34Wrknz+0RLfZG8H+LIkXzzp9vFs0P+895uqemqSZyW5\neXaS1Vocnr01yT1JPtTdG/X6krw2yc8k+ZPpQdakk/xmVR2rquumh1mx70lyIsmbFqcQbqiqC6eH\nWqNrk7xteoiT7eUA12nu26i9i/2iqr4zybuSXN/d90/Ps0rd/XB3PzPJ5UmeW1Ubcyqhql6S5J7u\nPjY9yxpd2d3PTnJNkp9YnBbaFAeSPDvJ67v7WUm+lmTjrqVJksXh9Zcmecf0LCfbywE+nuTJJ92+\nPMldQ7Pw/2lxbvRdSd7S3e+enmddFof2Ppzk6uFRVunKJC9dnCe9MckLquo3Zkdare6+a/HPe5K8\nJzunvjbF8STHTzoq887sBHkTXZPklu7+g+lBTraXA/yxJN9bVU9b/HVzbZL3Ds/EWVhcpPSGJLd3\n9y9Oz7NqVXWwqi5efP/4JC9M8pnZqVanu3+2uy/v7qdm5/fvt7v7R4bHWpmqunBxcWAWh2Z/IMnG\nvCOhu+9O8sWqumJx11VJNuICyNN4WR5lh5+TnUMQe1J3P1RVr0zywSTnJXljd982PNZKVdXbkvz1\nJE+squNJDnf3G2anWqkrk7w8yScX50mT5Oe6+/2DM63SJUnevLj68juSvL27N+6tOhvsSUnes/N3\nYg4keWt33zQ70sr9ZJK3LHZi7kjyY8PzrFxVXZCdd8v8+PQsp9qzb0MCgL1sLx+CBoA9S4ABYIAA\nA8AAAQaAAQIMAAMEGAAGCDAADPg/v2hxZuiP1asAAAAASUVORK5CYII=\n", + "image/png": "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\n", "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -732,14 +706,7 @@ } ], "source": [ - "display_NQueensCSP(solution)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The gray cells indicate the positions of the queens." + "plot_NQueens(solution)" ] }, { @@ -751,14 +718,14 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAeAAAAHwCAYAAAB+ArwOAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4wLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvpW3flQAAFaFJREFUeJzt3G2spAd53+H/Ha95sWPiNmwptikQ\nNbJEUQPsgRS5oi2GxA6UVH2RTBsUoqpO25DgNmpK8mWXKpXafIhIRYviGAhJAIvXilpgQpTQFLUx\nnDWmYAwVGEcsi+N1E9eAG4ydux/OuF2WXc5sM7O3z5zrko72zMwzz7nHj8a/87zMqe4OAHBufcf0\nAACwHwkwAAwQYAAYIMAAMECAAWCAAAPAAAEGgAECDAADBBjOgap6WlW9v6r+qKrurqrXV9WBb7P8\nxVX1hsWyD1TVJ6vqR8/lzMB6CTCcG/8hyT1JnpzkWUn+WpJ/eroFq+oxSX4ryVOTPD/JdyX5F0l+\noap+6pxMC6ydAMO58fQk7+juP+7uu5PcnOQvnWHZVyT5C0n+Xnd/obu/0d03J/mpJD9fVRclSVV1\nVf3FR55UVb9aVT9/0u2XVtVtVXVfVf3XqvrLJz12SVW9u6pOVNUXTg57VR2pqndU1a9V1Veq6vaq\n2jrp8X9ZVV9aPPbZqrpyNf+JYH8RYDg3finJNVV1QVVdmuTq7ET4dF6c5APd/bVT7n93kguS/JXd\nflhVPSfJm5L8eJLvTvLLSd5XVY+tqu9I8p+SfCLJpUmuTHJdVf3gSat4WZIbk1yc5H1JXr9Y7+VJ\nXpXkud19UZIfTHLXbvMA30qA4dz4z9nZ470/ybEk20n+4xmWfWKSL596Z3c/lOTeJAeX+Hn/KMkv\nd/ct3f1wd78lydezE+/nJjnY3f+qux/s7juT/EqSa056/ke6+/3d/XCSX0/yfYv7H07y2CTPqKrz\nu/uu7v78EvMApxBgWLPFHucHk7wnyYXZCeyfSfJvz/CUe7NzrvjU9RxYPPfEEj/2qUl+enH4+b6q\nui/JU5JcsnjsklMe+7kkTzrp+Xef9P0DSR5XVQe6+3NJrktyJMk9VXVjVV2yxDzAKQQY1u/PZid+\nr+/ur3f3/0zy5iQ/dIblfyvJ1VV14Sn3/50k30jy0cXtB7JzSPoRf/6k77+Y5F9398UnfV3Q3W9f\n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\nAgwAAwQYAAYIMAAMEGAAGCDAADBAgAFggAADwAABBoABAgwAAwQYAAYIMAAMEGAAGCDAADBAgAFg\ngAADwAABBoABAgwAAwQYAAYIMAAMEGAAGCDAADCgunv1K606keT3V77iM3tiknvP4c8717y+vc3r\n2/s2/TV6fav11O4+uNtCawnwuVZV2929NT3Hunh9e5vXt/dt+mv0+mY4BA0AAwQYAAZsSoCvnx5g\nzby+vc3r2/s2/TV6fQM24hwwAOw1m7IHDAB7igADwIA9HeCquqqqPltVn6uq10zPs2pV9aaquqeq\nPjU9yzpU1VOq6neq6o6qur2qXj090ypV1eOq6qNV9YnF63vt9EzrUFXnVdXHq+qm6VlWraruqqpP\nVtVtVbU9Pc+qVdXFVfWuqvrM4n34/OmZVqmqLl9su0e+7q+q66bnesSePQdcVecl+R9JXpzkWJKP\nJXl5d396dLAVqqoXJPlqkl/r7mdOz7NqVfXkJE/u7lur6qIkR5P8rU3ZhlVVSS7s7q9W1flJPpLk\n1d39e8OjrVRV/fMkW0me0N0vnZ5nlarqriRb3b2Rf6Siqt6S5L909w1V9ZgkF3T3fdNzrcOiGV9K\n8v3dfS7/UNQZ7eU94Ocl+Vx339ndDya5MckPD8+0Ut39u0n+cHqOdenuL3f3rYvvv5LkjiSXzk61\nOr3jq4ub5y++9uZvvGdQVZcleUmSG6Zn4exU1ROSvCDJG5Okux/c1PguXJnk84+W+CZ7O8CXJvni\nSbePZYP+573fVNXTkjw7yS2zk6zW4vDsbUnuSfKh7t6o15fkdUl+JsmfTA+yJp3kN6vqaFVdOz3M\nin1PkhNJ3rw4hXBDVV04PdQaXZPk7dNDnGwvB7hOc99G7V3sF1X1nUneneS67r5/ep5V6u6Hu/tZ\nSS5L8ryq2phTCVX10iT3dPfR6VnW6Irufk6Sq5P8xOK00KY4kOQ5Sd7Q3c9O8rUkG3ctTZIsDq+/\nLMk7p2c52V4O8LEkTznp9mVJjg/Nwv+nxbnRdyd5a3e/Z3qedVkc2vtwkquGR1mlK5K8bHGe9MYk\nL6yq35gdabW6+/ji33uSvDc7p742xbEkx046KvOu7AR5E12d5Nbu/oPpQU62lwP8sSTfW1VPX/x2\nc02S9w3PxFlYXKT0xiR3dPcvTs+zalV1sKouXnz/+CQvSvKZ2alWp7t/trsv6+6nZef999vd/SPD\nY61MVV24uDgwi0OzP5BkYz6R0N13J/liVV2+uOvKJBtxAeRpvDyPssPPyc4hiD2pux+qqlcl+WCS\n85K8qbtvHx5rparq7Un+epInVtWxJIe7+42zU63UFUlekeSTi/OkSfJz3f3+wZlW6clJ3rK4+vI7\nkryjuzfuozob7ElJ3rvze2IOJHlbd988O9LK/WSSty52Yu5M8mPD86xcVV2QnU/L/Pj0LKfasx9D\nAoC9bC8fggaAPUuAAWCAAAPAAAEGgAECDAADBBgABggwAAz4PyWycpsM6xLVAAAAAElFTkSuQmCC\n", + "image/png": "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\n", "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -768,7 +735,7 @@ "source": [ "eight_queens = NQueensCSP(8)\n", "solution = min_conflicts(eight_queens)\n", - "display_NQueensCSP(solution)" + "plot_NQueens(solution)" ] }, { diff --git a/images/queen_s.png b/images/queen_s.png new file mode 100644 index 0000000000000000000000000000000000000000..cc693102aec1e78cf865bea5249941886d48cb89 GIT binary patch literal 14407 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