diff --git a/gui/genetic_algorithm_example.py b/gui/genetic_algorithm_example.py new file mode 100644 index 000000000..418da02e9 --- /dev/null +++ b/gui/genetic_algorithm_example.py @@ -0,0 +1,172 @@ +# author: ad71 +# A simple program that implements the solution to the phrase generation problem using +# genetic algorithms as given in the search.ipynb notebook. +# +# Type on the home screen to change the target phrase +# Click on the slider to change genetic algorithm parameters +# Click 'GO' to run the algorithm with the specified variables +# Displays best individual of the current generation +# Displays a progress bar that indicates the amount of completion of the algorithm +# Displays the first few individuals of the current generation + +import sys +import time +import random +import os.path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +from tkinter import * +from tkinter import ttk + +import search +from utils import argmax + +LARGE_FONT = ('Verdana', 12) +EXTRA_LARGE_FONT = ('Consolas', 36, 'bold') + +canvas_width = 800 +canvas_height = 600 + +black = '#000000' +white = '#ffffff' +p_blue = '#042533' +lp_blue = '#0c394c' + +# genetic algorithm variables +# feel free to play around with these +target = 'Genetic Algorithm' # the phrase to be generated +max_population = 100 # number of samples in each population +mutation_rate = 0.1 # probability of mutation +f_thres = len(target) # fitness threshold +ngen = 1200 # max number of generations to run the genetic algorithm + +generation = 0 # counter to keep track of generation number + +u_case = [chr(x) for x in range(65, 91)] # list containing all uppercase characters +l_case = [chr(x) for x in range(97, 123)] # list containing all lowercase characters +punctuations1 = [chr(x) for x in range(33, 48)] # lists containing punctuation symbols +punctuations2 = [chr(x) for x in range(58, 65)] +punctuations3 = [chr(x) for x in range(91, 97)] +numerals = [chr(x) for x in range(48, 58)] # list containing numbers + +# extend the gene pool with the required lists and append the space character +gene_pool = [] +gene_pool.extend(u_case) +gene_pool.extend(l_case) +gene_pool.append(' ') + +# callbacks to update global variables from the slider values +def update_max_population(slider_value): + global max_population + max_population = slider_value + +def update_mutation_rate(slider_value): + global mutation_rate + mutation_rate = slider_value + +def update_f_thres(slider_value): + global f_thres + f_thres = slider_value + +def update_ngen(slider_value): + global ngen + ngen = slider_value + +# fitness function +def fitness_fn(_list): + fitness = 0 + # create string from list of characters + phrase = ''.join(_list) + # add 1 to fitness value for every matching character + for i in range(len(phrase)): + if target[i] == phrase[i]: + fitness += 1 + return fitness + +# function to bring a new frame on top +def raise_frame(frame, init=False, update_target=False, target_entry=None, f_thres_slider=None): + frame.tkraise() + global target + if update_target and target_entry is not None: + target = target_entry.get() + f_thres_slider.config(to=len(target)) + if init: + population = search.init_population(max_population, gene_pool, len(target)) + genetic_algorithm_stepwise(population) + +# defining root and child frames +root = Tk() +f1 = Frame(root) +f2 = Frame(root) + +# pack frames on top of one another +for frame in (f1, f2): + frame.grid(row=0, column=0, sticky='news') + +# Home Screen (f1) widgets +target_entry = Entry(f1, font=('Consolas 46 bold'), exportselection=0, foreground=p_blue, justify=CENTER) +target_entry.insert(0, target) +target_entry.pack(expand=YES, side=TOP, fill=X, padx=50) +target_entry.focus_force() + +max_population_slider = Scale(f1, from_=3, to=1000, orient=HORIZONTAL, label='Max population', command=lambda value: update_max_population(int(value))) +max_population_slider.set(max_population) +max_population_slider.pack(expand=YES, side=TOP, fill=X, padx=40) + +mutation_rate_slider = Scale(f1, from_=0, to=1, orient=HORIZONTAL, label='Mutation rate', resolution=0.0001, command=lambda value: update_mutation_rate(float(value))) +mutation_rate_slider.set(mutation_rate) +mutation_rate_slider.pack(expand=YES, side=TOP, fill=X, padx=40) + +f_thres_slider = Scale(f1, from_=0, to=len(target), orient=HORIZONTAL, label='Fitness threshold', command=lambda value: update_f_thres(int(value))) +f_thres_slider.set(f_thres) +f_thres_slider.pack(expand=YES, side=TOP, fill=X, padx=40) + +ngen_slider = Scale(f1, from_=1, to=5000, orient=HORIZONTAL, label='Max number of generations', command=lambda value: update_ngen(int(value))) +ngen_slider.set(ngen) +ngen_slider.pack(expand=YES, side=TOP, fill=X, padx=40) + +button = ttk.Button(f1, text='RUN', command=lambda: raise_frame(f2, init=True, update_target=True, target_entry=target_entry, f_thres_slider=f_thres_slider)).pack(side=BOTTOM, pady=50) + +# f2 widgets +canvas = Canvas(f2, width=canvas_width, height=canvas_height) +canvas.pack(expand=YES, fill=BOTH, padx=20, pady=15) +button = ttk.Button(f2, text='EXIT', command=lambda: raise_frame(f1)).pack(side=BOTTOM, pady=15) + +# function to run the genetic algorithm and update text on the canvas +def genetic_algorithm_stepwise(population): + root.title('Genetic Algorithm') + for generation in range(ngen): + # generating new population after selecting, recombining and mutating the existing population + population = [search.mutate(search.recombine(*search.select(2, population, fitness_fn)), gene_pool, mutation_rate) for i in range(len(population))] + # genome with the highest fitness in the current generation + current_best = ''.join(argmax(population, key=fitness_fn)) + # collecting first few examples from the current population + members = [''.join(x) for x in population][:48] + + # clear the canvas + canvas.delete('all') + # displays current best on top of the screen + canvas.create_text(canvas_width / 2, 40, fill=p_blue, font='Consolas 46 bold', text=current_best) + + # displaying a part of the population on the screen + for i in range(len(members) // 3): + canvas.create_text((canvas_width * .175), (canvas_height * .25 + (25 * i)), fill=lp_blue, font='Consolas 16', text=members[3 * i]) + canvas.create_text((canvas_width * .500), (canvas_height * .25 + (25 * i)), fill=lp_blue, font='Consolas 16', text=members[3 * i + 1]) + canvas.create_text((canvas_width * .825), (canvas_height * .25 + (25 * i)), fill=lp_blue, font='Consolas 16', text=members[3 * i + 2]) + + # displays current generation number + canvas.create_text((canvas_width * .5), (canvas_height * 0.95), fill=p_blue, font='Consolas 18 bold', text=f'Generation {generation}') + + # displays blue bar that indicates current maximum fitness compared to maximum possible fitness + scaling_factor = fitness_fn(current_best) / len(target) + canvas.create_rectangle(canvas_width * 0.1, 90, canvas_width * 0.9, 100, outline=p_blue) + canvas.create_rectangle(canvas_width * 0.1, 90, canvas_width * 0.1 + scaling_factor * canvas_width * 0.8, 100, fill=lp_blue) + canvas.update() + + # checks for completion + fittest_individual = search.fitness_threshold(fitness_fn, f_thres, population) + if fittest_individual: + break + +raise_frame(f1) +root.mainloop() \ No newline at end of file diff --git a/gui/tsp.py b/gui/tsp.py new file mode 100644 index 000000000..1830cba23 --- /dev/null +++ b/gui/tsp.py @@ -0,0 +1,346 @@ +from tkinter import * +from tkinter import messagebox +import sys +import os.path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +from search import * +import utils +import numpy as np + +distances = {} + + +class TSP_problem(Problem): + + """ subclass of Problem to define various functions """ + + def two_opt(self, state): + """ Neighbour generating function for Traveling Salesman Problem """ + neighbour_state = state[:] + left = random.randint(0, len(neighbour_state) - 1) + right = random.randint(0, len(neighbour_state) - 1) + if left > right: + left, right = right, left + neighbour_state[left: right + 1] = reversed(neighbour_state[left: right + 1]) + return neighbour_state + + def actions(self, state): + """ action that can be excuted in given state """ + return [self.two_opt] + + def result(self, state, action): + """ result after applying the given action on the given state """ + return action(state) + + def path_cost(self, c, state1, action, state2): + """ total distance for the Traveling Salesman to be covered if in state2 """ + cost = 0 + for i in range(len(state2) - 1): + cost += distances[state2[i]][state2[i + 1]] + cost += distances[state2[0]][state2[-1]] + return cost + + def value(self, state): + """ value of path cost given negative for the given state """ + return -1 * self.path_cost(None, None, None, state) + + +class TSP_Gui(): + """ Class to create gui of Traveling Salesman using simulated annealing where one can + select cities, change speed and temperature. Distances between cities are euclidean + distances between them. + """ + + def __init__(self, root, all_cities): + self.root = root + self.vars = [] + self.frame_locations = {} + self.calculate_canvas_size() + self.button_text = StringVar() + self.button_text.set("Start") + self.algo_var = StringVar() + self.all_cities = all_cities + self.frame_select_cities = Frame(self.root) + self.frame_select_cities.grid(row=1) + self.frame_canvas = Frame(self.root) + self.frame_canvas.grid(row=2) + Label(self.root, text="Map of Romania", font="Times 13 bold").grid(row=0, columnspan=10) + + def create_checkboxes(self, side=LEFT, anchor=W): + """ To select cities which are to be a part of Traveling Salesman Problem """ + + row_number = 0 + column_number = 0 + + for city in self.all_cities: + var = IntVar() + var.set(1) + Checkbutton(self.frame_select_cities, text=city, variable=var).grid( + row=row_number, column=column_number, sticky=W) + + self.vars.append(var) + column_number += 1 + if column_number == 10: + column_number = 0 + row_number += 1 + + def create_buttons(self): + """ Create start and quit button """ + + Button(self.frame_select_cities, textvariable=self.button_text, + command=self.run_traveling_salesman).grid(row=5, column=4, sticky=E + W) + Button(self.frame_select_cities, text='Quit', command=self.on_closing).grid( + row=5, column=5, sticky=E + W) + + def create_dropdown_menu(self): + """ Create dropdown menu for algorithm selection """ + + choices = {'Simulated Annealing', 'Genetic Algorithm', 'Hill Climbing'} + self.algo_var.set('Simulated Annealing') + dropdown_menu = OptionMenu(self.frame_select_cities, self.algo_var, *choices) + dropdown_menu.grid(row=4, column=4, columnspan=2, sticky=E + W) + dropdown_menu.config(width=19) + + def run_traveling_salesman(self): + """ Choose selected citites """ + + cities = [] + for i in range(len(self.vars)): + if self.vars[i].get() == 1: + cities.append(self.all_cities[i]) + + tsp_problem = TSP_problem(cities) + self.button_text.set("Reset") + self.create_canvas(tsp_problem) + + def calculate_canvas_size(self): + """ Width and height for canvas """ + + minx, maxx = sys.maxsize, -1 * sys.maxsize + miny, maxy = sys.maxsize, -1 * sys.maxsize + + for value in romania_map.locations.values(): + minx = min(minx, value[0]) + maxx = max(maxx, value[0]) + miny = min(miny, value[1]) + maxy = max(maxy, value[1]) + + # New locations squeezed to fit inside the map of romania + for name, coordinates in romania_map.locations.items(): + self.frame_locations[name] = (coordinates[0] / 1.2 - minx + + 150, coordinates[1] / 1.2 - miny + 165) + + canvas_width = maxx - minx + 200 + canvas_height = maxy - miny + 200 + + self.canvas_width = canvas_width + self.canvas_height = canvas_height + + def create_canvas(self, problem): + """ creating map with cities """ + + map_canvas = Canvas(self.frame_canvas, width=self.canvas_width, height=self.canvas_height) + map_canvas.grid(row=3, columnspan=10) + current = Node(problem.initial) + map_canvas.delete("all") + self.romania_image = PhotoImage(file="../images/romania_map.png") + map_canvas.create_image(self.canvas_width / 2, self.canvas_height / 2, + image=self.romania_image) + cities = current.state + for city in cities: + x = self.frame_locations[city][0] + y = self.frame_locations[city][1] + map_canvas.create_oval(x - 3, y - 3, x + 3, y + 3, + fill="red", outline="red") + map_canvas.create_text(x - 15, y - 10, text=city) + + self.cost = StringVar() + Label(self.frame_canvas, textvariable=self.cost, relief="sunken").grid( + row=2, columnspan=10) + + self.speed = IntVar() + speed_scale = Scale(self.frame_canvas, from_=500, to=1, orient=HORIZONTAL, + variable=self.speed, label="Speed ----> ", showvalue=0, font="Times 11", + relief="sunken", cursor="gumby") + speed_scale.grid(row=1, columnspan=5, sticky=N + S + E + W) + + if self.algo_var.get() == 'Simulated Annealing': + self.temperature = IntVar() + temperature_scale = Scale(self.frame_canvas, from_=100, to=0, orient=HORIZONTAL, + length=200, variable=self.temperature, label="Temperature ---->", + font="Times 11", relief="sunken", showvalue=0, cursor="gumby") + temperature_scale.grid(row=1, column=5, columnspan=5, sticky=N + S + E + W) + self.simulated_annealing_with_tunable_T(problem, map_canvas) + elif self.algo_var.get() == 'Genetic Algorithm': + self.mutation_rate = DoubleVar() + self.mutation_rate.set(0.05) + mutation_rate_scale = Scale(self.frame_canvas, from_=0, to=1, orient=HORIZONTAL, + length=200, variable=self.mutation_rate, label='Mutation Rate ---->', + font='Times 11', relief='sunken', showvalue=0, cursor='gumby', resolution=0.001) + mutation_rate_scale.grid(row=1, column=5, columnspan=5, sticky='nsew') + self.genetic_algorithm(problem, map_canvas) + elif self.algo_var.get() == 'Hill Climbing': + self.no_of_neighbors = IntVar() + self.no_of_neighbors.set(100) + no_of_neighbors_scale = Scale(self.frame_canvas, from_=10, to=1000, orient=HORIZONTAL, + length=200, variable=self.no_of_neighbors, label='Number of neighbors ---->', + font='Times 11',relief='sunken', showvalue=0, cursor='gumby') + no_of_neighbors_scale.grid(row=1, column=5, columnspan=5, sticky='nsew') + self.hill_climbing(problem, map_canvas) + + def exp_schedule(k=100, lam=0.03, limit=1000): + """ One possible schedule function for simulated annealing """ + + return lambda t: (k * math.exp(-lam * t) if t < limit else 0) + + def simulated_annealing_with_tunable_T(self, problem, map_canvas, schedule=exp_schedule()): + """ Simulated annealing where temperature is taken as user input """ + + current = Node(problem.initial) + + while(1): + T = schedule(self.temperature.get()) + if T == 0: + return current.state + neighbors = current.expand(problem) + if not neighbors: + return current.state + next = random.choice(neighbors) + delta_e = problem.value(next.state) - problem.value(current.state) + if delta_e > 0 or probability(math.exp(delta_e / T)): + map_canvas.delete("poly") + + current = next + self.cost.set("Cost = " + str('%0.3f' % (-1 * problem.value(current.state)))) + points = [] + for city in current.state: + points.append(self.frame_locations[city][0]) + points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(points, outline='red', width=3, fill='', tag="poly") + map_canvas.update() + map_canvas.after(self.speed.get()) + + def genetic_algorithm(self, problem, map_canvas): + """ Genetic Algorithm modified for the given problem """ + + def init_population(pop_number, gene_pool, state_length): + """ initialize population """ + + population = [] + for i in range(pop_number): + population.append(utils.shuffled(gene_pool)) + return population + + def recombine(state_a, state_b): + """ recombine two problem states """ + + start = random.randint(0, len(state_a) - 1) + end = random.randint(start + 1, len(state_a)) + new_state = state_a[start:end] + for city in state_b: + if city not in new_state: + new_state.append(city) + return new_state + + def mutate(state, mutation_rate): + """ mutate problem states """ + + if random.uniform(0, 1) < mutation_rate: + sample = random.sample(range(len(state)), 2) + state[sample[0]], state[sample[1]] = state[sample[1]], state[sample[0]] + return state + + def fitness_fn(state): + """ calculate fitness of a particular state """ + + fitness = problem.value(state) + return int((5600 + fitness) ** 2) + + current = Node(problem.initial) + population = init_population(100, current.state, len(current.state)) + all_time_best = current.state + while(1): + population = [mutate(recombine(*select(2, population, fitness_fn)), self.mutation_rate.get()) for i in range(len(population))] + current_best = utils.argmax(population, key=fitness_fn) + if fitness_fn(current_best) > fitness_fn(all_time_best): + all_time_best = current_best + self.cost.set("Cost = " + str('%0.3f' % (-1 * problem.value(all_time_best)))) + map_canvas.delete('poly') + points = [] + for city in current_best: + points.append(self.frame_locations[city][0]) + points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(points, outline='red', width=1, fill='', tag='poly') + best_points = [] + for city in all_time_best: + best_points.append(self.frame_locations[city][0]) + best_points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(best_points, outline='red', width=3, fill='', tag='poly') + map_canvas.update() + map_canvas.after(self.speed.get()) + + def hill_climbing(self, problem, map_canvas): + """ hill climbing where number of neighbors is taken as user input """ + + def find_neighbors(state, number_of_neighbors=100): + """ finds neighbors using two_opt method """ + + neighbors = [] + for i in range(number_of_neighbors): + new_state = problem.two_opt(state) + neighbors.append(Node(new_state)) + state = new_state + return neighbors + + current = Node(problem.initial) + while(1): + neighbors = find_neighbors(current.state, self.no_of_neighbors.get()) + neighbor = utils.argmax_random_tie(neighbors, key=lambda node: problem.value(node.state)) + map_canvas.delete('poly') + points = [] + for city in current.state: + points.append(self.frame_locations[city][0]) + points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(points, outline='red', width=3, fill='', tag='poly') + neighbor_points = [] + for city in neighbor.state: + neighbor_points.append(self.frame_locations[city][0]) + neighbor_points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(neighbor_points, outline='red', width=1, fill='', tag='poly') + map_canvas.update() + map_canvas.after(self.speed.get()) + if problem.value(neighbor.state) > problem.value(current.state): + current.state = neighbor.state + self.cost.set("Cost = " + str('%0.3f' % (-1 * problem.value(current.state)))) + + def on_closing(self): + if messagebox.askokcancel('Quit', 'Do you want to quit?'): + self.root.destroy() + +def main(): + all_cities = [] + for city in romania_map.locations.keys(): + distances[city] = {} + all_cities.append(city) + all_cities.sort() + + # distances['city1']['city2'] contains euclidean distance between their coordinates + for name_1, coordinates_1 in romania_map.locations.items(): + for name_2, coordinates_2 in romania_map.locations.items(): + distances[name_1][name_2] = np.linalg.norm( + [coordinates_1[0] - coordinates_2[0], coordinates_1[1] - coordinates_2[1]]) + distances[name_2][name_1] = np.linalg.norm( + [coordinates_1[0] - coordinates_2[0], coordinates_1[1] - coordinates_2[1]]) + + root = Tk() + root.title("Traveling Salesman Problem") + cities_selection_panel = TSP_Gui(root, all_cities) + cities_selection_panel.create_checkboxes() + cities_selection_panel.create_buttons() + cities_selection_panel.create_dropdown_menu() + root.protocol('WM_DELETE_WINDOW', cities_selection_panel.on_closing) + root.mainloop() + + +if __name__ == '__main__': + main() diff --git a/gui/xy_vacuum_environment.py b/gui/xy_vacuum_environment.py index 72d2f2434..14c3abc1a 100644 --- a/gui/xy_vacuum_environment.py +++ b/gui/xy_vacuum_environment.py @@ -11,6 +11,7 @@ class Gui(VacuumEnvironment): dirty, clean or can have a wall. The user can change these at each step. """ xi, yi = (0, 0) + perceptible_distance = 1 def __init__(self, root, width=7, height=7, elements=['D', 'W']): super().__init__(width, height) @@ -122,6 +123,20 @@ def update_env(self): self.step() xf, yf = agt.location + def reset_env(self, agt): + """Resets the GUI environment to the intial state.""" + self.read_env() + for i, btn_row in enumerate(self.buttons): + for j, btn in enumerate(btn_row): + if (i != 0 and i != len(self.buttons) - 1) and (j != 0 and j != len(btn_row) - 1): + if self.some_things_at((i, j)): + for thing in self.list_things_at((i, j)): + self.delete_thing(thing) + btn.config(text='', state='normal') + self.add_thing(agt, location=(3, 3)) + self.buttons[3][3].config( + text='A', state='disabled', disabledforeground='black') + def XYReflexAgentProgram(percept): """The modified SimpleReflexAgentProgram for the GUI environment.""" @@ -151,7 +166,9 @@ def __init__(self, program=None): self.direction = Direction("up") -# TODO: Check the coordinate system. +# TODO: +# Check the coordinate system. +# Give manual choice for agent's location. def main(): """The main function.""" root = Tk() @@ -159,10 +176,9 @@ def main(): root.geometry("420x440") root.resizable(0, 0) frame = Frame(root, bg='black') - # create a reset button - # reset_button = Button(frame, text='Reset', height=2, - # width=6, padx=2, pady=2, command=None) - # reset_button.pack(side='left') + reset_button = Button(frame, text='Reset', height=2, + width=6, padx=2, pady=2) + reset_button.pack(side='left') next_button = Button(frame, text='Next', height=2, width=6, padx=2, pady=2) next_button.pack(side='left') @@ -171,6 +187,7 @@ def main(): agt = XYReflexAgent(program=XYReflexAgentProgram) env.add_thing(agt, location=(3, 3)) next_button.config(command=env.update_env) + reset_button.config(command=lambda: env.reset_env(agt)) root.mainloop() diff --git a/images/romania_map.png b/images/romania_map.png new file mode 100644 index 000000000..426c76f1e Binary files /dev/null and b/images/romania_map.png differ diff --git a/search-4e.ipynb b/search-4e.ipynb index c7286c88b..73da69119 100644 --- a/search-4e.ipynb +++ b/search-4e.ipynb @@ -825,6 +825,7 @@ " def __init__(self, initial, LIFO=False):\n", " \"\"\"Initialize Frontier with an initial Node.\n", " If LIFO is True, pop from the end first; otherwise from front first.\"\"\"\n", + " super(FrontierQ, self).__init__()\n", " self.LIFO = LIFO\n", " self.add(initial)\n", " \n", diff --git a/search.ipynb b/search.ipynb index 019ea8eb4..ac621b622 100644 --- a/search.ipynb +++ b/search.ipynb @@ -15,11 +15,13 @@ "cell_type": "code", "execution_count": 1, "metadata": { + "collapsed": true, "scrolled": true }, "outputs": [], "source": [ "from search import *\n", + "from notebook import psource\n", "\n", "# Needed to hide warnings in the matplotlib sections\n", "import warnings\n", @@ -39,6 +41,7 @@ "* Breadth-First Search\n", "* Uniform Cost Search\n", "* A\\* Search\n", + "* Best First Search\n", "* Genetic Algorithm" ] }, @@ -80,7 +83,9 @@ { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "%psource Problem" @@ -120,7 +125,9 @@ { "cell_type": "code", "execution_count": 3, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "%psource GraphProblem" @@ -136,7 +143,9 @@ { "cell_type": "code", "execution_count": 4, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "romania_map = UndirectedGraph(dict(\n", @@ -181,7 +190,9 @@ { "cell_type": "code", "execution_count": 5, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)" @@ -212,11 +223,7 @@ "name": "stdout", "output_type": "stream", "text": [ -<<<<<<< HEAD - "{'Rimnicu': (233, 410), 'Timisoara': (94, 410), 'Iasi': (473, 506), 'Neamt': (406, 537), 'Fagaras': (305, 449), 'Giurgiu': (375, 270), 'Urziceni': (456, 350), 'Mehadia': (168, 339), 'Lugoj': (165, 379), 'Sibiu': (207, 457), 'Oradea': (131, 571), 'Zerind': (108, 531), 'Craiova': (253, 288), 'Hirsova': (534, 350), 'Arad': (91, 492), 'Vaslui': (509, 444), 'Drobeta': (165, 299), 'Bucharest': (400, 327), 'Eforie': (562, 293), 'Pitesti': (320, 368)}\n" -======= "{'Oradea': (131, 571), 'Eforie': (562, 293), 'Timisoara': (94, 410), 'Hirsova': (534, 350), 'Bucharest': (400, 327), 'Rimnicu': (233, 410), 'Fagaras': (305, 449), 'Lugoj': (165, 379), 'Giurgiu': (375, 270), 'Mehadia': (168, 339), 'Pitesti': (320, 368), 'Drobeta': (165, 299), 'Craiova': (253, 288), 'Sibiu': (207, 457), 'Iasi': (473, 506), 'Urziceni': (456, 350), 'Vaslui': (509, 444), 'Neamt': (406, 537), 'Zerind': (108, 531), 'Arad': (91, 492)}\n" ->>>>>>> 8561c52d63fcaef4c0f99d997073aeb93e926e56 ] } ], @@ -235,26 +242,10 @@ { "cell_type": "code", "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "ename": "ImportError", - "evalue": "No module named 'matplotlib'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mImportError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun_line_magic\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'matplotlib'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'inline'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mnetworkx\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mnx\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mmatplotlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpyplot\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mplt\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mmatplotlib\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mlines\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.5/site-packages/IPython/core/interactiveshell.py\u001b[0m in \u001b[0;36mrun_line_magic\u001b[0;34m(self, magic_name, line, _stack_depth)\u001b[0m\n\u001b[1;32m 2093\u001b[0m \u001b[0mkwargs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'local_ns'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msys\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_getframe\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstack_depth\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mf_locals\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2094\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbuiltin_trap\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 2095\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2096\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2097\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m\u001b[0m in \u001b[0;36mmatplotlib\u001b[0;34m(self, line)\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.5/site-packages/IPython/core/magic.py\u001b[0m in \u001b[0;36m\u001b[0;34m(f, *a, **k)\u001b[0m\n\u001b[1;32m 185\u001b[0m \u001b[0;31m# but it's overkill for just that one bit of state.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 186\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mmagic_deco\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 187\u001b[0;31m \u001b[0mcall\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mlambda\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 188\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 189\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcallable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.5/site-packages/IPython/core/magics/pylab.py\u001b[0m in \u001b[0;36mmatplotlib\u001b[0;34m(self, line)\u001b[0m\n\u001b[1;32m 97\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Available matplotlib backends: %s\"\u001b[0m \u001b[0;34m%\u001b[0m \u001b[0mbackends_list\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 98\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 99\u001b[0;31m \u001b[0mgui\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbackend\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshell\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0menable_matplotlib\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgui\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 100\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_show_matplotlib_backend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mgui\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbackend\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 101\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.5/site-packages/IPython/core/interactiveshell.py\u001b[0m in \u001b[0;36menable_matplotlib\u001b[0;34m(self, gui)\u001b[0m\n\u001b[1;32m 2964\u001b[0m \"\"\"\n\u001b[1;32m 2965\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mIPython\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcore\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mpylabtools\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0mpt\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 2966\u001b[0;31m \u001b[0mgui\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbackend\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpt\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfind_gui_and_backend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgui\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpylab_gui_select\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2967\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2968\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mgui\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0;34m'inline'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/.local/lib/python3.5/site-packages/IPython/core/pylabtools.py\u001b[0m in \u001b[0;36mfind_gui_and_backend\u001b[0;34m(gui, gui_select)\u001b[0m\n\u001b[1;32m 268\u001b[0m \"\"\"\n\u001b[1;32m 269\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 270\u001b[0;31m \u001b[0;32mimport\u001b[0m \u001b[0mmatplotlib\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 271\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 272\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mgui\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mgui\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0;34m'auto'\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mImportError\u001b[0m: No module named 'matplotlib'" - ] - } - ], + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "%matplotlib inline\n", "import networkx as nx\n", @@ -277,20 +268,10 @@ { "cell_type": "code", "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'nx' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# initialise a graph\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mG\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnx\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mGraph\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;31m# use this while labeling nodes in the map\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mnode_labels\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mNameError\u001b[0m: name 'nx' is not defined" - ] - } - ], + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "# initialise a graph\n", "G = nx.Graph()\n", @@ -429,7 +410,9 @@ { "cell_type": "code", "execution_count": 11, - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [ { "data": { @@ -465,7 +448,7 @@ "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\n", + "5. Best First Graph Search - Implemented\n", "6. Uniform Cost Search - Implemented\n", "7. Depth Limited Search\n", "8. Iterative Deepening Search\n", @@ -1208,7 +1191,7 @@ ], "source": [ "all_node_colors = []\n", - "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", + "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", "display_visual(user_input = False, algorithm = astar_search, problem = romania_problem)" ] }, @@ -1275,181 +1258,201 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## A* Search Heuristics Comparison\n", + "## BEST FIRST SEARCH\n", + "Let's change all the node_colors to starting position and define a different problem statement." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "def best_first_graph_search(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", + " first search; if f is node.depth then we have breadth-first search.\n", + " There is a subtlety: the line \"f = memoize(f, 'f')\" means that the f\n", + " values will be cached on the nodes as they are computed. So after doing\n", + " a best first search you can examine the f values of the path returned.\"\"\"\n", + " \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", + " \n", + " f = memoize(f, 'f')\n", + " node = Node(problem.initial)\n", + " \n", + " node_colors[node.state] = \"red\"\n", + " iterations += 1\n", + " all_node_colors.append(dict(node_colors))\n", + " \n", + " if problem.goal_test(node.state):\n", + " node_colors[node.state] = \"green\"\n", + " iterations += 1\n", + " all_node_colors.append(dict(node_colors))\n", + " return(iterations, all_node_colors, node)\n", + " \n", + " frontier = PriorityQueue(min, f)\n", + " frontier.append(node)\n", + " \n", + " node_colors[node.state] = \"orange\"\n", + " iterations += 1\n", + " all_node_colors.append(dict(node_colors))\n", + " \n", + " explored = set()\n", + " while frontier:\n", + " node = frontier.pop()\n", + " \n", + " node_colors[node.state] = \"red\"\n", + " iterations += 1\n", + " all_node_colors.append(dict(node_colors))\n", + " \n", + " if problem.goal_test(node.state):\n", + " node_colors[node.state] = \"green\"\n", + " iterations += 1\n", + " all_node_colors.append(dict(node_colors))\n", + " return(iterations, all_node_colors, node)\n", + " \n", + " explored.add(node.state)\n", + " for child in node.expand(problem):\n", + " if child.state not in explored and child not in frontier:\n", + " frontier.append(child)\n", + " node_colors[child.state] = \"orange\"\n", + " iterations += 1\n", + " all_node_colors.append(dict(node_colors))\n", + " elif child in frontier:\n", + " incumbent = frontier[child]\n", + " if f(child) < f(incumbent):\n", + " del frontier[incumbent]\n", + " frontier.append(child)\n", + " node_colors[child.state] = \"orange\"\n", + " iterations += 1\n", + " all_node_colors.append(dict(node_colors))\n", "\n", - "Different Heuristics have different efficiency in solving a particular problem via A* search which is generally defined by the node of explored nodes as well as the branching factor. With the help of the Classic 8* Puzzle we can effectively visualize the difference in performance of these heuristics. \n", + " node_colors[node.state] = \"gray\"\n", + " iterations += 1\n", + " all_node_colors.append(dict(node_colors))\n", + " return None\n", "\n", - "### 8-Puzzle Problem\n", + "def best_first_search(problem, h=None):\n", + " \"\"\"Best-first graph search is an informative searching algorithm with f(n) = h(n).\n", + " 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)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5ae2d521b74743afa988c462a851c269" + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "559c20b044a4469db7f0ab8c3fae1022" + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "all_node_colors = []\n", + "romania_problem = GraphProblem('Arad', 'Bucharest', romania_map)\n", + "display_visual(user_input = False, algorithm = best_first_search, problem = romania_problem)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## A* Heuristics\n", "\n", - "*8-Puzzle Problem* is another problem that is classified as NP hard for which genetic algorithms provide a better solution than any pre-existing ones.\n", + "Different heuristics provide different efficiency in solving A* problems which are generally defined by the number of explored nodes as well as the branching factor. With the classic 8 puzzle we can show the efficiency of different heuristics through the number of explored nodes.\n", "\n", - "The *8-Puzzle Problem* consists of a *3x3 tray* in which 8 tiles numbered 1-8 are placed and the 9th tile is uncovered. The aim of the game is that given a initial placement of the tiles, we have to reach the goal state on the constraint that a tile adjacent to be the blank space can be slid into that space.\n", + "### 8 Puzzle Problem\n", "\n", - "*example:*\n", - " Initial State Goal State\n", + "The *8 Puzzle Problem* consists of a 3x3 tray in which the goal is to get the initial configuration to the goal state by shifting the numbered tiles into the blank space.\n", "\n", - " | 7 | 2 | 4 | | | 1 | 2 |\n", - " | 5 | | 6 | ----> | 3 | 4 | 5 |\n", - " | 8 | 3 | 1 | | 6 | 7 | 8 |\n", + "example:- \n", "\n", - "We have a total of 8+1(blank) tiles giving us total of 9! initial configurations but of all these configurations only 9!/2 can lead to a solution.The solvability can be checked by calculating the *Permutation Inversion* of each tile and then summing it up.\n", - "Inversion is defined as when a tile preceeds another tile with lower number.\n", - "Let's calculate the Permutation Inversion of the example shown above -\n", - " \n", - " Tile 7 -> 6 Inversions (for tile 2, 4, 5, 6, 3, 1)\n", - " Tile 2 -> 1 Inversions\n", - " Tile 4 -> 2 Inversions\n", - " Tile 5 -> 2 Inversions\n", - " Tile 6 -> 2 Inversions\n", - " Tile 8 -> 2 Inversions\n", - " Tile 3 -> 1 Inversions\n", - " Tile 1 -> 0 Inversions\n", - "Total Inversions = 16 Inversions, \n", - "Is total Inversions are even then the initial configuration is solvable else the configuration is impossible to solve.\n", + " Initial State Goal State\n", + " | 7 | 2 | 4 | | 0 | 1 | 2 |\n", + " | 5 | 0 | 6 | | 3 | 4 | 5 |\n", + " | 8 | 3 | 1 | | 6 | 7 | 8 |\n", + " \n", + "We have a total of 9 blank tiles giving us a total of 9! initial configuration but not all of these are solvable, the solvability of a configuration can be checked by calculating the Inversion Permutation. If the total Inversion Permutation is even then the initial configuration is solvable else the initial configuration is not solvable which means that only 9!/2 initial states lead to a solution.\n", "\n", - "For example we can have a state \"724506831\" where 0 represents the empty tile.\n", + "#### Heuristics :-\n", "\n", - "#### Heuristics:-\n", - "1.) Manhattan Distance:- For the 8 Puzzle problem \"Manhattan distance is defined as the distance of a tile from its \n", - " goal. In the example shown above the manhattan distance for the 'numbered tile 1' is 4\n", - " (2 unit left and 2 unit up).\n", + "1.) Manhattan Distance:- For the 8 puzzle problem Manhattan distance is defined as the distance of a tile from its goal state( for the tile numbered '1' in the initial configuration Manhattan distance is 4 \"2 for left and 2 for upward displacement\").\n", "\n", - "2.) No. of Misplaced Tiles:- This heuristics calculates the number of misplaced tile in the state from the goal \n", - " state.\n", + "2.) No. of Misplaced Tiles:- The heuristic calculates the number of misplaced tiles between the current state and goal state.\n", "\n", - "3.) Sqrt of Manhattan Distance:- Uses the sqaure root of the Manhattan distance\n", + "3.) Sqrt of Manhattan Distance:- It calculates the square root of Manhattan distance.\n", "\n", - "4.) Max Heuristic :- Score on the basis of max of Manhattan Distance and No. of Misplced tiles." + "4.) Max Heuristic:- It assign the score as max of Manhattan Distance and No. of misplaced tiles. " ] }, { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ - "# define heuristics\n", + "# heuristics for 8 Puzzle Problem\n", + "\n", "def linear(state,goal):\n", " return sum([1 if state[i] != goal[i] else 0 for i in range(8)])\n", "\n", "def manhanttan(state,goal):\n", - " index_goal = {0:[2,2], 1:[0,0], 2:[0,1], 3:[0,2], 4:[1,0], 5:[1,1], 6:[1,2], 7:[2,0], 8:[2,1]}\n", - " index_state = {}\n", - " index = [[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]]\n", - " x=0\n", - " y=0\n", - " for i in range(len(state)):\n", - " index_state[state[i]] = index[i]\n", - " mhd = 0\n", - " for i in range(8):\n", - " for j in range(2):\n", - " mhd = abs(index_goal[i][j] - index_state[i][j]) + mhd\n", - " return mhd\n", + "\tindex_goal = {0:[2,2], 1:[0,0], 2:[0,1], 3:[0,2], 4:[1,0], 5:[1,1], 6:[1,2], 7:[2,0], 8:[2,1]}\n", + "\tindex_state = {}\n", + "\tindex = [[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]]\n", + "\tx=0\n", + "\ty=0\n", + "\tfor i in range(len(state)):\n", + "\t\tindex_state[state[i]] = index[i]\n", + "\tmhd = 0\n", + "\tfor i in range(8):\n", + "\t\tfor j in range(2):\n", + "\t\t\tmhd = abs(index_goal[i][j] - index_state[i][j]) + mhd\n", + "\treturn mhd\n", "\n", "def sqrt_manhanttan(state,goal):\n", - " index_goal = {0:[2,2], 1:[0,0], 2:[0,1], 3:[0,2], 4:[1,0], 5:[1,1], 6:[1,2], 7:[2,0], 8:[2,1]}\n", - " index_state = {}\n", - " index = [[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]]\n", - " x=0\n", - " y=0\n", - " for i in range(len(state)):\n", - " index_state[state[i]] = index[i]\n", - " mhd = 0\n", - " for i in range(8):\n", - " for j in range(2):\n", - " mhd = (index_goal[i][j] - index_state[i][j])**2 + mhd\n", - " return math.sqrt(mhd)\n", + "\tindex_goal = {0:[2,2], 1:[0,0], 2:[0,1], 3:[0,2], 4:[1,0], 5:[1,1], 6:[1,2], 7:[2,0], 8:[2,1]}\n", + "\tindex_state = {}\n", + "\tindex = [[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]]\n", + "\tx=0\n", + "\ty=0\n", + "\tfor i in range(len(state)):\n", + "\t\tindex_state[state[i]] = index[i]\n", + "\tmhd = 0\n", + "\tfor i in range(8):\n", + "\t\tfor j in range(2):\n", + "\t\t\tmhd = (index_goal[i][j] - index_state[i][j])**2 + mhd\n", + "\treturn math.sqrt(mhd)\n", "\n", "def max_heuristic(state,goal):\n", - " score1 = manhanttan(state, goal)\n", - " score2 = linear(state, goal)\n", - " return max(score1, score2)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# Algorithm for 8 Puzzle problem\n", - "\n", - "def checkSolvability(state):\n", - " inversion = 0\n", - " for i in range(len(state)):\n", - " for j in range(i,len(state)):\n", - " if (state[i]>state[j] and state[j]!=0):\n", - " inversion += 1\n", - " check = True\n", - " if inversion%2 != 0:\n", - " check = False\n", - " print(check)\n", - " return check\n", - "\n", - "def getPossibleMoves(state,heuristic,goal,moves):\n", - " move = {0:[1,3], 1:[0,2,4], 2:[1,5], 3:[0,6,4], 4:[1,3,5,7], 5:[2,4,8], 6:[3,7], 7:[6,8], 8:[7,5]} # create a dictionary of moves\n", - " index = state[0].index(0)\n", - " possible_moves = []\n", - " for i in range(len(move[index])):\n", - " conf = list(state[0][:])\n", - " a = conf[index]\n", - " b = conf[move[index][i]]\n", - " conf[move[index][i]] = a\n", - " conf[index] = b\n", - " possible_moves.append(conf)\n", - " scores = []\n", - " for i in possible_moves:\n", - " scores.append(heuristic(i,goal))\n", - " scores = [x+moves for x in scores]\n", - " allowed_state = []\n", - " for i in range(len(possible_moves)):\n", - " node = []\n", - " node.append(possible_moves[i])\n", - " node.append(scores[i])\n", - " node.append(state[0])\n", - " allowed_state.append(node) \n", - " return allowed_state\n", - "\n", - "path = []\n", - "final = []\n", - "def create_path(goal,initial):\n", - " node = goal[0]\n", - " final.append(goal[0])\n", - " if goal[2] == initial:\n", - " return reversed(final)\n", - " else:\n", - " parent = goal[2]\n", - " for i in path:\n", - " if i[0] == parent:\n", - " parent = i\n", - " create_path(parent,initial)\t\n", - "\n", - "def show_path(initial):\n", - " move = []\n", - " for i in range(0,len(path)):\n", - " move.append(''.join(str(x) for x in path[i][0]))\n", - " print(\"Number of explored nodes by the following heuristic are: \", len(set(move)))\t\n", - " print(initial)\n", - " for i in reversed(final):\n", - " print(i)\n", - " return\n", - "\n", - "def solve(initial,goal,heuristic):\n", - " root = [initial,heuristic(initial,goal),'']\n", - " nodes = [] # nodes is a priority Queue based on the state score \n", - " nodes.append(root)\n", - " moves = 0\n", - " while len(nodes) != 0:\n", - " node = nodes[0]\n", - " del nodes[0]\n", - " path.append(node)\n", - " if node[0] == goal:\n", - " soln = create_path(path[-1],initial )\n", - " show_path(initial)\n", - " return \n", - " moves +=1\n", - " opened_nodes = getPossibleMoves(node,heuristic,goal,moves)\n", - " nodes = sorted(opened_nodes+nodes, key=itemgetter(1))\n" + "\tscore1 = manhanttan(state, goal)\n", + "\tscore2 = linear(state, goal)\n", + "\treturn max(score1, score2)\t\t\n" ] }, { @@ -1461,7 +1464,6 @@ "name": "stdout", "output_type": "stream", "text": [ - "Heuristics is max_heuristic\n", "True\n", "Number of explored nodes by the following heuristic are: 126\n", "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", @@ -1472,16 +1474,48 @@ "[1, 2, 3, 0, 4, 5, 7, 8, 6]\n", "[1, 2, 3, 4, 0, 5, 7, 8, 6]\n", "[1, 2, 3, 4, 5, 0, 7, 8, 6]\n", + "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n", + "Number of explored nodes by the following heuristic are: 129\n", + "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", + "[2, 4, 3, 1, 5, 0, 7, 8, 6]\n", + "[2, 4, 3, 1, 0, 5, 7, 8, 6]\n", + "[2, 0, 3, 1, 4, 5, 7, 8, 6]\n", + "[0, 2, 3, 1, 4, 5, 7, 8, 6]\n", + "[1, 2, 3, 0, 4, 5, 7, 8, 6]\n", + "[1, 2, 3, 4, 0, 5, 7, 8, 6]\n", + "[1, 2, 3, 4, 5, 0, 7, 8, 6]\n", + "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n", + "Number of explored nodes by the following heuristic are: 126\n", + "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", + "[2, 4, 3, 1, 5, 0, 7, 8, 6]\n", + "[2, 4, 3, 1, 0, 5, 7, 8, 6]\n", + "[2, 0, 3, 1, 4, 5, 7, 8, 6]\n", + "[0, 2, 3, 1, 4, 5, 7, 8, 6]\n", + "[1, 2, 3, 0, 4, 5, 7, 8, 6]\n", + "[1, 2, 3, 4, 0, 5, 7, 8, 6]\n", + "[1, 2, 3, 4, 5, 0, 7, 8, 6]\n", + "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n", + "Number of explored nodes by the following heuristic are: 139\n", + "[2, 4, 3, 1, 5, 6, 7, 8, 0]\n", + "[2, 4, 3, 1, 5, 0, 7, 8, 6]\n", + "[2, 4, 3, 1, 0, 5, 7, 8, 6]\n", + "[2, 0, 3, 1, 4, 5, 7, 8, 6]\n", + "[0, 2, 3, 1, 4, 5, 7, 8, 6]\n", + "[1, 2, 3, 0, 4, 5, 7, 8, 6]\n", + "[1, 2, 3, 4, 0, 5, 7, 8, 6]\n", + "[1, 2, 3, 4, 5, 0, 7, 8, 6]\n", "[1, 2, 3, 4, 5, 6, 7, 8, 0]\n" ] } ], "source": [ - "goal_state = [1,2,3,4,5,6,7,8,0] # define the goal state\n", - "initial_state = [2,4,3,1,5,6,7,8,0] # define the initial state\n", - "print(\"Heuristics is max_heuristic\")\n", - "checkSolvability(initial_state)\n", - "solve(initial_state,goal_state,max_heuristic) # to check the different heuristics change the function name in solve" + "# Solving the puzzle \n", + "puzzle = EightPuzzle()\n", + "puzzle.checkSolvability([2,4,3,1,5,6,7,8,0]) # checks whether the initialized configuration is solvable or not\n", + "puzzle.solve([2,4,3,1,5,6,7,8,0],[1,2,3,4,5,6,7,8,0],max_heuristic) # Max_heuristic\n", + "puzzle.solve([2,4,3,1,5,6,7,8,0],[1,2,3,4,5,6,7,8,0],linear) # Linear\n", + "puzzle.solve([2,4,3,1,5,6,7,8,0],[1,2,3,4,5,6,7,8,0],manhanttan) # Manhattan\n", + "puzzle.solve([2,4,3,1,5,6,7,8,0],[1,2,3,4,5,6,7,8,0],sqrt_manhanttan) # Sqrt_manhattan" ] }, { @@ -1595,9 +1629,122 @@ "cell_type": "code", "execution_count": 2, "metadata": {}, - "outputs": [], + "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" + } + ], "source": [ - "%psource genetic_algorithm" + "psource(genetic_algorithm)" ] }, { @@ -1627,16 +1774,121 @@ "source": [ "For each generation, the algorithm updates the population. First it calculates the fitnesses of the individuals, then it selects the most fit ones and finally crosses them over to produce offsprings. There is a chance that the offspring will be mutated, given by `pmut`. If at the end of the generation an individual meets the fitness threshold, the algorithm halts and returns that individual.\n", "\n", - "The function of mating is accomplished by the method `reproduce`:" + "The function of mating is accomplished by the method `recombine`:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [], + "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" + } + ], "source": [ - "%psource reproduce" + "psource(recombine)" ] }, { @@ -1652,9 +1904,121 @@ "cell_type": "code", "execution_count": 4, "metadata": {}, - "outputs": [], + "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" + } + ], "source": [ - "%psource mutate" + "psource(mutate)" ] }, { @@ -1670,9 +2034,122 @@ "cell_type": "code", "execution_count": 5, "metadata": {}, - "outputs": [], + "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" + } + ], "source": [ - "%psource init_population" + "psource(init_population)" ] }, { @@ -1686,40 +2163,557 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Usage\n", + "### Explanation\n", "\n", - "Below we give two example usages for the genetic algorithm, for a graph coloring problem and the 8 queens problem.\n", + "Before we solve problems using the genetic algorithm, we will explain how to intuitively understand the algorithm using a trivial exmaple.\n", "\n", - "#### Graph Coloring\n", + "#### Generating Phrases\n", "\n", - "First we will take on the simpler problem of coloring a small graph with two colors. Before we do anything, let's imagine how a solution might look. First, we have to represent our colors. Say, 'R' for red and 'G' for green. These make up our gene pool. What of the individual solutions though? For that, we will look at our problem. We stated we have a graph. A graph has nodes and edges, and we want to color the nodes. Naturally, we want to store each node's color. If we have four nodes, we can store their colors in a list of genes, one for each node. A possible solution will then look like this: ['R', 'R', 'G', 'R']. In the general case, we will represent each solution with a list of chars ('R' and 'G'), with length the number of nodes.\n", + "In this problem, we use a genetic algorithm to generate a particular target phrase from a population of random strings. This is a classic example that helps build intuition about how to use this algorithm in other problems as well. Before we break the problem down, let us try to brute force the solution. Let us say that we want to generate the phrase \"genetic algorithm\". The phrase is 17 characters long. We can use any character from the 26 lowercase characters and the space character. To generate a random phrase of length 17, each space can be filled in 27 ways. So the total number of possible phrases is\n", "\n", - "Next we need to come up with a fitness function that appropriately scores individuals. Again, we will look at the problem definition at hand. We want to color a graph. For a solution to be optimal, no edge should connect two nodes of the same color. How can we use this information to score a solution? A naive (and ineffective) approach would be to count the different colors in the string. So ['R', 'R', 'R', 'R'] has a score of 1 and ['R', 'R', 'G', 'G'] has a score of 2. Why that fitness function is not ideal though? Why, we forgot the information about the edges! The edges are pivotal to the problem and the above function only deals with node colors. We didn't use all the information at hand and ended up with an ineffective answer. How, then, can we use that information to our advantage?\n", + "$$ 27^{17} = 2153693963075557766310747 $$\n", "\n", - "We said that the optimal solution will have all the edges connecting nodes of different color. So, to score a solution we can count how many edges are valid (aka connecting nodes of different color). That is a great fitness function!\n", + "which is a massive number. If we wanted to generate the phrase \"Genetic Algorithm\", we would also have to include all the 26 uppercase characters into consideration thereby increasing the sample space from 27 characters to 53 characters and the total number of possible phrases then would be\n", "\n", - "Let's jump into solving this problem using the `genetic_algorithm` function." + "$$ 53^{17} = 205442259656281392806087233013 $$\n", + "\n", + "If we wanted to include punctuations and numerals into the sample space, we would have further complicated an already impossible problem. Hence, brute forcing is not an option. Now we'll apply the genetic algorithm and see how it significantly reduces the search space. We essentially want to *evolve* our population of random strings so that they better approximate the target phrase as the number of generations increase. Genetic algorithms work on the principle of Darwinian Natural Selection according to which, there are three key concepts that need to be in place for evolution to happen. They are:\n", + "\n", + "1. Heredity : There must be a process in place by which children receive the properties of their parents.
\n", + "For this particular problem, two strings from the population will be chosen as parents and will be split at a random index and recombined as described in the `recombine` function to create a child. This child string will then be added to the new generation.\n", + "
\n",
+    "
\n", + "2. Variation : There must be a variety of traits present in the population or a means with which to introduce variation.
If there is no variation in the sample space, we might never reach the global optimum. To ensure that there is enough variation, we can initialize a large population, but this gets computationally expensive as the population gets larger. Hence, we often use another method called mutation. In this method, we randomly change one or more characters of some strings in the population based on a predefined probability value called the mutation rate or mutation probability as described in the `mutate` function. The mutation rate is usually kept quite low. A mutation rate of zero fails to introduce variation in the population and a high mutation rate (say 50%) is as good as a coin flip and the population fails to benefit from the previous recombinations. An optimum balance has to be maintained between population size and mutation rate so as to reduce the computational cost as well as have sufficient variation in the population.\n", + "
\n",
+    "
\n", + "3. Selection : There must be some mechanism by which some members of the population have the opportunity to be parents and pass down their genetic information and some do not. This is typically referred to as \"survival of the fittest\".
\n", + "There has to be some way of determining which phrases in our population have a better chance of eventually evolving into the target phrase. This is done by introducing a fitness function that calculates how close the generated phrase is to the target phrase. The function will simply return a scalar value corresponding to the number of matching characters between the generated phrase and the target phrase." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "First we need to represent the graph. Since we mostly need information about edges, we will just store the edges. We will denote edges with capital letters and nodes with integers:" + "Before solving the problem, we first need to define our target phrase." ] }, { "cell_type": "code", - "execution_count": 12, - "metadata": {}, + "execution_count": 33, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ - "edges = {\n", - " 'A': [0, 1],\n", - " 'B': [0, 3],\n", - " 'C': [1, 2],\n", - " 'D': [2, 3]\n", - "}" + "target = 'Genetic Algorithm'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "We then need to define our gene pool, i.e the elements which an individual from the population might comprise of. Here, the gene pool contains all uppercase and lowercase letters of the English alphabet and the space character." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# The ASCII values of uppercase characters ranges from 65 to 91\n", + "u_case = [chr(x) for x in range(65, 91)]\n", + "# The ASCII values of lowercase characters ranges from 97 to 123\n", + "l_case = [chr(x) for x in range(97, 123)]\n", + "\n", + "gene_pool = []\n", + "gene_pool.extend(u_case) # adds the uppercase list to the gene pool\n", + "gene_pool.extend(l_case) # adds the lowercase list to the gene pool\n", + "gene_pool.append(' ') # adds the space character to the gene pool" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to define the maximum size of each population. Larger populations have more variation but are computationally more expensive to run algorithms on." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "max_population = 100" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As our population is not very large, we can afford to keep a relatively large mutation rate." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "mutation_rate = 0.07 # 7%" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Great! Now, we need to define the most important metric for the genetic algorithm, i.e the fitness function. This will simply return the number of matching characters between the generated sample and the target phrase." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def fitness_fn(sample):\n", + " # initialize fitness to 0\n", + " fitness = 0\n", + " for i in range(len(sample)):\n", + " # increment fitness by 1 for every matching character\n", + " if sample[i] == target[i]:\n", + " fitness += 1\n", + " return fitness" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before we run our genetic algorithm, we need to initialize a random population. We will use the `init_population` function to do this. We need to pass in the maximum population size, the gene pool and the length of each individual, which in this case will be the same as the length of the target phrase." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "population = init_population(max_population, gene_pool, len(target))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now define how the individuals in the population should change as the number of generations increases. First, the `select` function will be run on the population to select *two* individuals with high fitness values. These will be the parents which will then be recombined using the `recombine` function to generate the child." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "parents = select(2, population, fitness_fn) " + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# The recombine function takes two parents as arguments, so we need to unpack the previous variable\n", + "child = recombine(*parents)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we need to apply a mutation according to the mutation rate. We call the `mutate` function on the child with the gene pool and mutation rate as the additional arguments." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "child = mutate(child, gene_pool, mutation_rate)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above lines can be condensed into\n", + "\n", + "`child = mutate(recombine(*select(2, population, fitness_fn)), gene_pool, mutation_rate)`\n", + "\n", + "And, we need to do this `for` every individual in the current population to generate the new population." + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "population = [mutate(recombine(*select(2, population, fitness_fn)), gene_pool, mutation_rate) for i in range(len(population))]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The individual with the highest fitness can then be found using the `max` function." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "current_best = max(population, key=fitness_fn)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's print this out" + ] + }, + { + "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" + ] + } + ], + "source": [ + "print(current_best)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We see that this is a list of characters. This can be converted to a string using the join function" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "jFmFNicvmjVodrtVH\n" + ] + } + ], + "source": [ + "current_best_string = ''.join(current_best)\n", + "print(current_best_string)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to define the conditions to terminate the algorithm. This can happen in two ways\n", + "1. Termination after a predefined number of generations\n", + "2. Termination when the fitness of the best individual of the current generation reaches a predefined threshold value.\n", + "\n", + "We define these variables below" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "ngen = 1200 # maximum number of generations\n", + "# we set the threshold fitness equal to the length of the target phrase\n", + "# i.e the algorithm only terminates whne it has got all the characters correct \n", + "# or it has completed 'ngen' number of generations\n", + "f_thres = len(target)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "To generate `ngen` number of generations, we run a `for` loop `ngen` number of times. After each generation, we calculate the fitness of the best individual of the generation and compare it to the value of `f_thres` using the `fitness_threshold` function. After every generation, we print out the best individual of the generation and the corresponding fitness value. Lets now write a function to do this." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def genetic_algorithm_stepwise(population, fitness_fn, gene_pool=[0, 1], f_thres=None, ngen=1200, pmut=0.1):\n", + " for generation in range(ngen):\n", + " population = [mutate(recombine(*select(2, population, fitness_fn)), gene_pool, pmut) for i in range(len(population))]\n", + " # stores the individual genome with the highest fitness in the current population\n", + " current_best = ''.join(max(population, key=fitness_fn))\n", + " print(f'Current best: {current_best}\\t\\tGeneration: {str(generation)}\\t\\tFitness: {fitness_fn(current_best)}\\r', end='')\n", + " \n", + " # compare the fitness of the current best individual to f_thres\n", + " fittest_individual = fitness_threshold(fitness_fn, f_thres, population)\n", + " \n", + " # if fitness is greater than or equal to f_thres, we terminate the algorithm\n", + " if fittest_individual:\n", + " return fittest_individual, generation\n", + " return max(population, key=fitness_fn) , generation " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The function defined above is essentially the same as the one defined in `search.py` with the added functionality of printing out the data of each generation." + ] + }, + { + "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" + } + ], + "source": [ + "psource(genetic_algorithm)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have defined all the required functions and variables. Let's now create a new population and test the function we wrote above." + ] + }, + { + "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" + ] + } + ], + "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)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The genetic algorithm was able to converge!\n", + "We implore you to rerun the above cell and play around with `target, max_population, f_thres, ngen` etc parameters to get a better intuition of how the algorithm works. To summarize, if we can define the problem states in simple array format and if we can create a fitness function to gauge how good or bad our approximate solutions are, there is a high chance that we can get a satisfactory solution using a genetic algorithm. \n", + "- There is also a better GUI version of this program `genetic_algorithm_example.py` in the GUI folder for you to play around with." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Usage\n", + "\n", + "Below we give two example usages for the genetic algorithm, for a graph coloring problem and the 8 queens problem.\n", + "\n", + "#### Graph Coloring\n", + "\n", + "First we will take on the simpler problem of coloring a small graph with two colors. Before we do anything, let's imagine how a solution might look. First, we have to represent our colors. Say, 'R' for red and 'G' for green. These make up our gene pool. What of the individual solutions though? For that, we will look at our problem. We stated we have a graph. A graph has nodes and edges, and we want to color the nodes. Naturally, we want to store each node's color. If we have four nodes, we can store their colors in a list of genes, one for each node. A possible solution will then look like this: ['R', 'R', 'G', 'R']. In the general case, we will represent each solution with a list of chars ('R' and 'G'), with length the number of nodes.\n", + "\n", + "Next we need to come up with a fitness function that appropriately scores individuals. Again, we will look at the problem definition at hand. We want to color a graph. For a solution to be optimal, no edge should connect two nodes of the same color. How can we use this information to score a solution? A naive (and ineffective) approach would be to count the different colors in the string. So ['R', 'R', 'R', 'R'] has a score of 1 and ['R', 'R', 'G', 'G'] has a score of 2. Why that fitness function is not ideal though? Why, we forgot the information about the edges! The edges are pivotal to the problem and the above function only deals with node colors. We didn't use all the information at hand and ended up with an ineffective answer. How, then, can we use that information to our advantage?\n", + "\n", + "We said that the optimal solution will have all the edges connecting nodes of different color. So, to score a solution we can count how many edges are valid (aka connecting nodes of different color). That is a great fitness function!\n", + "\n", + "Let's jump into solving this problem using the `genetic_algorithm` function." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First we need to represent the graph. Since we mostly need information about edges, we will just store the edges. We will denote edges with capital letters and nodes with integers:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "edges = {\n", + " 'A': [0, 1],\n", + " 'B': [0, 3],\n", + " 'C': [1, 2],\n", + " 'D': [2, 3]\n", + "}" ] }, { @@ -1733,14 +2727,14 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[['R', 'G', 'G', 'R'], ['G', 'R', 'G', 'G'], ['G', 'G', 'G', 'G'], ['R', 'G', 'G', 'G'], ['R', 'G', 'G', 'R'], ['G', 'R', 'G', 'R'], ['G', 'G', 'G', 'R'], ['G', 'R', 'G', 'R']]\n" + "[['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" ] } ], @@ -1760,8 +2754,10 @@ }, { "cell_type": "code", - "execution_count": 14, - "metadata": {}, + "execution_count": 8, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "def fitness(c):\n", @@ -1777,14 +2773,14 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "['G', 'R', 'G', 'R']\n" + "['R', 'G', 'R', 'G']\n" ] } ], @@ -1802,7 +2798,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -1847,14 +2843,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[[6, 7, 3, 6, 3, 0, 1, 4], [7, 1, 4, 1, 5, 2, 0, 0], [1, 4, 7, 0, 0, 2, 5, 2], [2, 0, 3, 7, 5, 7, 0, 0], [6, 3, 1, 7, 5, 6, 3, 0]]\n" + "[[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" ] } ], @@ -1878,8 +2874,10 @@ }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, + "execution_count": 12, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "def fitness(q):\n", @@ -1908,20 +2906,20 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[3, 5, 7, 2, 0, 6, 4, 1]\n", - "28\n" + "[5, 0, 6, 3, 7, 4, 1, 3]\n", + "26\n" ] } ], "source": [ - "solution = genetic_algorithm(population, fitness, f_thres=28, gene_pool=range(8))\n", + "solution = genetic_algorithm(population, fitness, f_thres=25, gene_pool=range(8))\n", "print(solution)\n", "print(fitness(solution))" ] @@ -1939,13 +2937,6 @@ "source": [ "With that this tutorial on the genetic algorithm comes to an end. Hope you found this guide helpful!" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -1964,424 +2955,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", -<<<<<<< HEAD - "version": "3.5.4rc1" -======= - "version": "3.5.2" ->>>>>>> 8561c52d63fcaef4c0f99d997073aeb93e926e56 - }, - "widgets": { - "state": { - "013d8df0a2ab4899b09f83aa70ce5d50": { - "views": [] - }, - "01ee7dc2239c4b0095710436453b362d": { - "views": [] - }, - "04d594ae6a704fc4b16895e6a7b85270": { - "views": [] - }, - "052ea3e7259346a4b022ec4fef1fda28": { - "views": [ - { - "cell_index": 32 - } - ] - }, - "0ade4328785545c2b66d77e599a3e9da": { - "views": [ - { - "cell_index": 29 - } - ] - }, - "0b94d8de6b4e47f89b0382b60b775cbd": { - "views": [] - }, - "0c63dcc0d11a451ead31a4c0c34d7b43": { - "views": [] - }, - "0d91be53b6474cdeac3239fdffeab908": { - "views": [ - { - "cell_index": 39 - } - ] - }, - "0fe9c3b9b1264d4abd22aef40a9c1ab9": { - "views": [] - }, - "10fd06131b05455d9f0a98072d7cebc6": { - "views": [] - }, - "1193eaa60bb64cb790236d95bf11f358": { - "views": [ - { - "cell_index": 38 - } - ] - }, - "11b596cbf81a47aabccae723684ac3a5": { - "views": [] - }, - "127ae5faa86f41f986c39afb320f2298": { - "views": [] - }, - "16a9167ec7b4479e864b2a32e40825a1": { - "views": [ - { - "cell_index": 39 - } - ] - }, - "170e2e101180413f953a192a41ecbfcc": { - "views": [] - }, - "181efcbccf89478792f0e38a25500e51": { - "views": [] - }, - "1894a28092604d69b0d7d465a3b165b1": { - "views": [] - }, - "1a56cc2ab5ae49ea8bf2a3f6ca2b1c36": { - "views": [] - }, - "1cfd8f392548467696d8cd4fc534a6b4": { - "views": [] - }, - "1e395e67fdec406f8698aa5922764510": { - "views": [] - }, - "23509c6536404e96985220736d286183": { - "views": [] - }, - "23bffaca1206421fb9ea589126e35438": { - "views": [] - }, - "25330d0b799e4f02af5e510bc70494cf": { - "views": [] - }, - "2ab8bf4795ac4240b70e1a94e14d1dd6": { - "views": [ - { - "cell_index": 30 - } - ] - }, - "2bd48f1234e4422aaedecc5815064181": { - "views": [] - }, - "2d3a082066304c8ebf2d5003012596b4": { - "views": [] - }, - "2dc962f16fd143c1851aaed0909f3963": { - "views": [ - { - "cell_index": 35 - } - ] - }, - "2f659054242a453da5ea0884de996008": { - "views": [] - }, - "30a214881db545729c1b883878227e95": { - "views": [] - }, - "3275b81616424947be98bf8fd3cd7b82": { - "views": [] - }, - "330b52bc309d4b6a9b188fd9df621180": { - "views": [] - }, - "3320648123f44125bcfda3b7c68febcf": { - "views": [] - }, - "338e3b1562e747f197ab3ceae91e371f": { - "views": [] - }, - "34658e2de2894f01b16cf89905760f14": { - "views": [ - { - "cell_index": 39 - } - ] - }, - "352f5fd9f698460ea372c6af57c5b478": { - "views": [] - }, - "35dc16b828a74356b56cd01ff9ddfc09": { - "views": [] - }, - "3805ce2994364bd1b259373d8798cc7a": { - "views": [] - }, - "3d1f1f899cfe49aaba203288c61686ac": { - "views": [] - }, - "3d7e943e19794e29b7058eb6bbe23c66": { - "views": [] - }, - "3f6652b3f85740949b7711fbcaa509ba": { - "views": [] - }, - "43e48664a76342c991caeeb2d5b17a49": { - "views": [ - { - "cell_index": 35 - } - ] - }, - "4662dec8595f45fb9ae061b2bdf44427": { - "views": [] - }, - "47ae3d2269d94a95a567be21064eb98a": { - "views": [] - }, - "49c49d665ba44746a1e1e9dc598bc411": { - "views": [ - { - "cell_index": 39 - } - ] - }, - "4a1c43b035f644699fd905d5155ad61f": { - "views": [ - { - "cell_index": 39 - } - ] - }, - "4eb88b6f6b4241f7b755f69b9e851872": { - "views": [] - }, - "4fbb3861e50f41c688e9883da40334d4": { - "views": [] - }, - "52d76de4ee8f4487b335a4a11726fbce": { - "views": [] - }, - "53eccc8fc0ad461cb8277596b666f32a": { - "views": [ - { - "cell_index": 29 - } - ] - }, - "54d3a6067b594ad08907ce059d9f4a41": { - "views": [] - }, - "612530d3edf8443786b3093ab612f88b": { - "views": [] - }, - "613a133b6d1f45e0ac9c5c270bc408e0": { - "views": [] - }, - "636caa7780614389a7f52ad89ea1c6e8": { - "views": [ - { - "cell_index": 39 - } - ] - }, - "63aa621196294629b884c896b6a034d8": { - "views": [] - }, - "66d1d894cc7942c6a91f0630fc4321f9": { - "views": [] - }, - "6775928a174b43ecbe12608772f1cb05": { - "views": [] - }, - "6bce621c90d543bca50afbe0c489a191": { - "views": [] - }, - "6ebbb8c7ec174c15a6ee79a3c5b36312": { - "views": [] - }, - "743219b9d37e4f47a5f777bb41ad0a96": { - "views": [ - { - "cell_index": 29 - } - ] - }, - "774f464794cc409ca6d1106bcaac0cf1": { - "views": [] - }, - "7ba3da40fb26490697fc64b3248c5952": { - "views": [] - }, - "7e79fea4654f4bedb5969db265736c25": { - "views": [] - }, - "85c82ed0844f4ae08a14fd750e55fc15": { - "views": [] - }, - "86e8f92c1d584cdeb13b36af1b6ad695": { - "views": [ - { - "cell_index": 35 - } - ] - }, - "88485e72d2ec447ba7e238b0a6de2839": { - "views": [] - }, - "892d7b895d3840f99504101062ba0f65": { - "views": [] - }, - "89be4167713e488696a20b9b5ddac9bd": { - "views": [] - }, - "8a24a07d166b45498b7d8b3f97c131eb": { - "views": [] - }, - "8e7c7f3284ee45b38d95fe9070d5772f": { - "views": [] - }, - "98985eefab414365991ed6844898677f": { - "views": [] - }, - "98df98e5af87474d8b139cb5bcbc9792": { - "views": [] - }, - "99f11243d387409bbad286dd5ecb1725": { - "views": [] - }, - "9ab2d641b0be4cf8950be5ba72e5039f": { - "views": [] - }, - "9b1ffbd1e7404cb4881380a99c7d11bc": { - "views": [] - }, - "9c07ec6555cb4d0ba8b59007085d5692": { - "views": [] - }, - "9cc80f47249b4609b98223ce71594a3d": { - "views": [] - }, - "9d79bfd34d3640a3b7156a370d2aabae": { - "views": [] - }, - "a015f138cbbe4a0cad4d72184762ed75": { - "views": [] - }, - "a27d2f1eb3834c38baf1181b0de93176": { - "views": [] - }, - "a29b90d050f3442a89895fc7615ccfee": { - "views": [ - { - "cell_index": 29 - } - ] - }, - "a725622cfc5b43b4ae14c74bc2ad7ad0": { - "views": [] - }, - "ac2e05d7d7e945bf99862a2d9d1fa685": { - "views": [] - }, - "b0bb2ca65caa47579a4d3adddd94504b": { - "views": [] - }, - "b8995c40625d465489e1b7ec8014b678": { - "views": [] - }, - "ba83da1373fe45d19b3c96a875f2f4fb": { - "views": [] - }, - "baa0040d35c64604858c529418c22797": { - "views": [] - }, - "badc9fd7b56346d6b6aea68bfa6d2699": { - "views": [ - { - "cell_index": 38 - } - ] - }, - "bdb41c7654e54c83a91452abc59141bd": { - "views": [] - }, - "c2399056ef4a4aa7aa4e23a0f381d64a": { - "views": [ - { - "cell_index": 38 - } - ] - }, - "c73b47b242b4485fb1462abcd92dc7c9": { - "views": [] - }, - "ce3f28a8aeee4be28362d068426a71f6": { - "views": [ - { - "cell_index": 32 - } - ] - }, - "d3067a6bb84544bba5f1abd241a72e55": { - "views": [] - }, - "db13a2b94de34ce9bea721aaf971c049": { - "views": [] - }, - "db468d80cb6e43b6b88455670b036618": { - "views": [] - }, - "e2cb458522b4438ea3f9873b6e411acb": { - "views": [] - }, - "e77dca31f1d94d4dadd3f95d2cdbf10e": { - "views": [] - }, - "e7bffb1fed664dea90f749ea79dcc4f1": { - "views": [ - { - "cell_index": 39 - } - ] - }, - "e80abb145fce4e888072b969ba8f455a": { - "views": [] - }, - "e839d0cf348c4c1b832fc1fc3b0bd3c9": { - "views": [] - }, - "e948c6baadde46f69f105649555b84eb": { - "views": [] - }, - "eb16e9da25bf4bef91a34b1d0565c774": { - "views": [] - }, - "ec82b64048834eafa3e53733bb54a713": { - "views": [] - }, - "edbb3a621c87445e9df4773cc60ec8d2": { - "views": [] - }, - "ef6c99705936425a975e49b9e18ac267": { - "views": [] - }, - "f1b494f025dd48d1ae58ae8e3e2ebf46": { - "views": [] - }, - "f435b108c59c42989bf209a625a3a5b5": { - "views": [ - { - "cell_index": 32 - } - ] - }, - "f71ed7e15a314c28973943046c4529d6": { - "views": [] - }, - "f81f726f001c4fb999851df532ed39f2": { - "views": [] - } - }, - "version": "1.1.1" + "version": "3.6.1" } }, "nbformat": 4, diff --git a/search.py b/search.py index 873c03752..1e32d5b8c 100644 --- a/search.py +++ b/search.py @@ -7,7 +7,7 @@ from utils import ( is_in, argmin, argmax, argmax_random_tie, probability, weighted_sampler, memoize, print_table, open_data, Stack, FIFOQueue, PriorityQueue, name, - distance + distance, vector_add ) from collections import defaultdict @@ -17,6 +17,7 @@ import bisect from operator import itemgetter + infinity = float('inf') # ______________________________________________________________________________ @@ -400,6 +401,95 @@ def astar_search(problem, h=None): h = memoize(h or problem.h, 'h') return best_first_graph_search(problem, lambda n: n.path_cost + h(n)) +# ______________________________________________________________________________ +# A* heuristics + +class EightPuzzle(): + + def __init__(self): + self.path = [] + self.final = [] + + def checkSolvability(self, state): + 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) + + def getPossibleMoves(self,state,heuristic,goal,moves): + move = {0:[1,3], 1:[0,2,4], 2:[1,5], 3:[0,6,4], 4:[1,3,5,7], 5:[2,4,8], 6:[3,7], 7:[6,8], 8:[7,5]} # create a dictionary of moves + index = state[0].index(0) + possible_moves = [] + for i in range(len(move[index])): + conf = list(state[0][:]) + a = conf[index] + b = conf[move[index][i]] + conf[move[index][i]] = a + conf[index] = b + possible_moves.append(conf) + scores = [] + for i in possible_moves: + scores.append(heuristic(i,goal)) + scores = [x+moves for x in scores] + allowed_state = [] + for i in range(len(possible_moves)): + node = [] + node.append(possible_moves[i]) + node.append(scores[i]) + node.append(state[0]) + allowed_state.append(node) + return allowed_state + + + def create_path(self,goal,initial): + node = goal[0] + self.final.append(goal[0]) + if goal[2] == initial: + return reversed(self.final) + else: + parent = goal[2] + for i in self.path: + if i[0] == parent: + parent = i + self.create_path(parent,initial) + + def show_path(self,initial): + move = [] + for i in range(0,len(self.path)): + move.append(''.join(str(x) for x in self.path[i][0])) + + print("Number of explored nodes by the following heuristic are: ", len(set(move))) + print(initial) + for i in reversed(self.final): + print(i) + + del self.path[:] + del self.final[:] + return + + def solve(self,initial,goal,heuristic): + root = [initial,heuristic(initial,goal),''] + nodes = [] # nodes is a priority Queue based on the state score + nodes.append(root) + moves = 0 + while len(nodes) != 0: + node = nodes[0] + del nodes[0] + self.path.append(node) + if node[0] == goal: + soln = self.create_path(self.path[-1],initial ) + self.show_path(initial) + return + moves +=1 + opened_nodes = self.getPossibleMoves(node,heuristic,goal,moves) + nodes = sorted(opened_nodes+nodes, key=itemgetter(1)) + + # ______________________________________________________________________________ # Other search algorithms @@ -526,39 +616,37 @@ def and_search(states, problem, path): # body of and or search return or_search(problem.initial, problem, []) +# Pre-defined actions for PeakFindingProblem +directions4 = { 'W':(-1, 0), 'N':(0, 1), 'E':(1, 0), 'S':(0, -1) } +directions8 = dict(directions4) +directions8.update({'NW':(-1, 1), 'NE':(1, 1), 'SE':(1, -1), 'SW':(-1, -1) }) class PeakFindingProblem(Problem): """Problem of finding the highest peak in a limited grid""" - def __init__(self, initial, grid): + def __init__(self, initial, grid, defined_actions=directions4): """The grid is a 2 dimensional array/list whose state is specified by tuple of indices""" Problem.__init__(self, initial) self.grid = grid + self.defined_actions = defined_actions self.n = len(grid) assert self.n > 0 self.m = len(grid[0]) assert self.m > 0 def actions(self, state): - """Allows movement in only 4 directions""" - # TODO: Add flag to allow diagonal motion + """Returns the list of actions which are allowed to be taken from the given state""" allowed_actions = [] - if state[0] > 0: - allowed_actions.append('N') - if state[0] < self.n - 1: - allowed_actions.append('S') - if state[1] > 0: - allowed_actions.append('W') - if state[1] < self.m - 1: - allowed_actions.append('E') + for action in self.defined_actions: + next_state = vector_add(state, self.defined_actions[action]) + if next_state[0] >= 0 and next_state[1] >= 0 and next_state[0] <= self.n - 1 and next_state[1] <= self.m - 1: + allowed_actions.append(action) + return allowed_actions def result(self, state, action): """Moves in the direction specified by action""" - x, y = state - x = x + (1 if action == 'S' else (-1 if action == 'N' else 0)) - y = y + (1 if action == 'E' else (-1 if action == 'W' else 0)) - return (x, y) + return vector_add(state, self.defined_actions[action]) def value(self, state): """Value of a state is the value it is the index to""" @@ -772,6 +860,19 @@ def recombine(x, y): return x[:c] + y[c:] +def recombine_uniform(x, y): + n = len(x) + result = [0] * n; + indexes = random.sample(range(n), n) + for i in range(n): + ix = indexes[i] + result[ix] = x[ix] if i < n / 2 else y[ix] + try: + return ''.join(result) + except: + return result + + def mutate(x, gene_pool, pmut): if random.uniform(0, 1) >= pmut: return x @@ -1347,3 +1448,4 @@ def compare_graph_searchers(): GraphProblem('Q', 'WA', australia_map)], header=['Searcher', 'romania_map(Arad, Bucharest)', 'romania_map(Oradea, Neamt)', 'australia_map']) + diff --git a/tests/test_search.py b/tests/test_search.py index f22ca6f89..04cb2db35 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -88,12 +88,12 @@ def test_hill_climbing(): def test_simulated_annealing(): random.seed("aima-python") prob = PeakFindingProblem((0, 0), [[0, 5, 10, 20], - [-3, 7, 11, 5]]) + [-3, 7, 11, 5]], directions4) sols = {prob.value(simulated_annealing(prob)) for i in range(100)} assert max(sols) == 20 prob = PeakFindingProblem((0, 0), [[0, 5, 10, 8], [-3, 7, 9, 999], - [1, 2, 5, 11]]) + [1, 2, 5, 11]], directions8) sols = {prob.value(simulated_annealing(prob)) for i in range(100)} assert max(sols) == 999