From 99928737455d878e42777675965a7efaca62f863 Mon Sep 17 00:00:00 2001 From: Rahul Goswami Date: Sat, 24 Mar 2018 02:24:49 +0530 Subject: [PATCH] refactored FIFOQueue, Stack, and PriorityQueue --- README.md | 2 +- gui/romania_problem.py | 152 ++++++++++++++++++++++------------- planning.py | 11 +-- search.py | 92 ++++++++++----------- tests/test_search.py | 8 +- tests/test_utils.py | 47 ----------- utils.py | 177 ++++++++++++++--------------------------- 7 files changed, 215 insertions(+), 274 deletions(-) diff --git a/README.md b/README.md index 41d08f431..e3aa1f9e4 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Here is a table of algorithms, the figure, name of the algorithm in the book and | 3.2 | Romania | `romania` | [`search.py`][search] | Done | Included | | 3.7 | Tree-Search | `tree_search` | [`search.py`][search] | Done | | | 3.7 | Graph-Search | `graph_search` | [`search.py`][search] | Done | | -| 3.11 | Breadth-First-Search | `breadth_first_search` | [`search.py`][search] | Done | Included | +| 3.11 | Breadth-First-Search | `breadth_first_graph_search` | [`search.py`][search] | Done | Included | | 3.14 | Uniform-Cost-Search | `uniform_cost_search` | [`search.py`][search] | Done | Included | | 3.17 | Depth-Limited-Search | `depth_limited_search` | [`search.py`][search] | Done | | | 3.18 | Iterative-Deepening-Search | `iterative_deepening_search` | [`search.py`][search] | Done | | diff --git a/gui/romania_problem.py b/gui/romania_problem.py index 67eced970..b1778eef9 100644 --- a/gui/romania_problem.py +++ b/gui/romania_problem.py @@ -5,9 +5,9 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '..')) from search import * from search import breadth_first_tree_search as bfts, depth_first_tree_search as dfts, \ - depth_first_graph_search as dfgs, breadth_first_search as bfs, uniform_cost_search as ucs, \ + depth_first_graph_search as dfgs, breadth_first_graph_search as bfs, uniform_cost_search as ucs, \ astar_search as asts -from utils import Stack, FIFOQueue, PriorityQueue +from utils import PriorityQueue from copy import deepcopy root = None @@ -26,9 +26,7 @@ def create_map(root): - ''' - This function draws out the required map. - ''' + """This function draws out the required map.""" global city_map, start, goal romania_locations = romania_map.locations width = 750 @@ -260,17 +258,13 @@ def create_map(root): def make_line(map, x0, y0, x1, y1, distance): - ''' - This function draws out the lines joining various points. - ''' + """This function draws out the lines joining various points.""" map.create_line(x0, y0, x1, y1) map.create_text((x0 + x1) / 2, (y0 + y1) / 2, text=distance) def make_rectangle(map, x0, y0, margin, city_name): - ''' - This function draws out rectangles for various points. - ''' + """This function draws out rectangles for various points.""" global city_coord rect = map.create_rectangle( x0 - margin, @@ -313,51 +307,51 @@ def make_legend(map): def tree_search(problem): - ''' + """ 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] This function has been changed to make it suitable for the Tkinter GUI. - ''' + """ global counter, frontier, node - # print(counter) + if counter == -1: frontier.append(Node(problem.initial)) - # print(frontier) + display_frontier(frontier) if counter % 3 == 0 and counter >= 0: node = frontier.pop() - # print(node) + display_current(node) if counter % 3 == 1 and counter >= 0: if problem.goal_test(node.state): - # print(node) + return node frontier.extend(node.expand(problem)) - # print(frontier) + display_frontier(frontier) if counter % 3 == 2 and counter >= 0: - # print(node) + display_explored(node) return None def graph_search(problem): - ''' + """ Search through the successors of a problem to find a goal. The argument frontier should be an empty queue. If two paths reach a state, only use the first one. [Figure 3.7] This function has been changed to make it suitable for the Tkinter GUI. - ''' + """ global counter, frontier, node, explored if counter == -1: frontier.append(Node(problem.initial)) explored = set() - # print("Frontier: "+str(frontier)) + display_frontier(frontier) if counter % 3 == 0 and counter >= 0: node = frontier.pop() - # print("Current node: "+str(node)) + display_current(node) if counter % 3 == 1 and counter >= 0: if problem.goal_test(node.state): @@ -366,18 +360,15 @@ def graph_search(problem): frontier.extend(child for child in node.expand(problem) if child.state not in explored and child not in frontier) - # print("Frontier: " + str(frontier)) + display_frontier(frontier) if counter % 3 == 2 and counter >= 0: - # print("Explored node: "+str(node)) display_explored(node) return None def display_frontier(queue): - ''' - This function marks the frontier nodes (orange) on the map. - ''' + """This function marks the frontier nodes (orange) on the map.""" global city_map, city_coord qu = deepcopy(queue) while qu: @@ -388,27 +379,21 @@ def display_frontier(queue): def display_current(node): - ''' - This function marks the currently exploring node (red) on the map. - ''' + """This function marks the currently exploring node (red) on the map.""" global city_map, city_coord city = node.state city_map.itemconfig(city_coord[city], fill="red") def display_explored(node): - ''' - This function marks the already explored node (gray) on the map. - ''' + """This function marks the already explored node (gray) on the map.""" global city_map, city_coord city = node.state city_map.itemconfig(city_coord[city], fill="gray") def display_final(cities): - ''' - This function marks the final solution nodes (green) on the map. - ''' + """This function marks the final solution nodes (green) on the map.""" global city_map, city_coord for city in cities: city_map.itemconfig(city_coord[city], fill="green") @@ -416,22 +401,56 @@ def display_final(cities): def breadth_first_tree_search(problem): """Search the shallowest nodes in the search tree first.""" - global frontier, counter + global frontier, counter, node if counter == -1: - frontier = FIFOQueue() - return tree_search(problem) + frontier = deque() + + if counter == -1: + frontier.append(Node(problem.initial)) + + display_frontier(frontier) + if counter % 3 == 0 and counter >= 0: + node = frontier.popleft() + + display_current(node) + if counter % 3 == 1 and counter >= 0: + if problem.goal_test(node.state): + return node + frontier.extend(node.expand(problem)) + + display_frontier(frontier) + if counter % 3 == 2 and counter >= 0: + display_explored(node) + return None def depth_first_tree_search(problem): """Search the deepest nodes in the search tree first.""" # This search algorithm might not work in case of repeated paths. - global frontier, counter + global frontier, counter, node if counter == -1: - frontier = Stack() - return tree_search(problem) + frontier = [] # stack + + if counter == -1: + frontier.append(Node(problem.initial)) + + display_frontier(frontier) + if counter % 3 == 0 and counter >= 0: + node = frontier.pop() + + display_current(node) + if counter % 3 == 1 and counter >= 0: + if problem.goal_test(node.state): + return node + frontier.extend(node.expand(problem)) + + display_frontier(frontier) + if counter % 3 == 2 and counter >= 0: + display_explored(node) + return None -def breadth_first_search(problem): +def breadth_first_graph_search(problem): """[Figure 3.11]""" global frontier, node, explored, counter if counter == -1: @@ -439,12 +458,13 @@ def breadth_first_search(problem): display_current(node) if problem.goal_test(node.state): return node - frontier = FIFOQueue() - frontier.append(node) + + frontier = deque([node]) # FIFO queue + display_frontier(frontier) explored = set() if counter % 3 == 0 and counter >= 0: - node = frontier.pop() + node = frontier.popleft() display_current(node) explored.add(node.state) if counter % 3 == 1 and counter >= 0: @@ -461,10 +481,30 @@ def breadth_first_search(problem): def depth_first_graph_search(problem): """Search the deepest nodes in the search tree first.""" - global frontier, counter + global counter, frontier, node, explored if counter == -1: - frontier = Stack() - return graph_search(problem) + frontier = [] # stack + if counter == -1: + frontier.append(Node(problem.initial)) + explored = set() + + display_frontier(frontier) + if counter % 3 == 0 and counter >= 0: + node = frontier.pop() + + display_current(node) + if counter % 3 == 1 and counter >= 0: + if problem.goal_test(node.state): + return node + explored.add(node.state) + frontier.extend(child for child in node.expand(problem) + if child.state not in explored and + child not in frontier) + + display_frontier(frontier) + if counter % 3 == 2 and counter >= 0: + display_explored(node) + return None def best_first_graph_search(problem, f): @@ -483,7 +523,7 @@ def best_first_graph_search(problem, f): display_current(node) if problem.goal_test(node.state): return node - frontier = PriorityQueue(min, f) + frontier = PriorityQueue('min', f) frontier.append(node) display_frontier(frontier) explored = set() @@ -525,9 +565,9 @@ def astar_search(problem, h=None): # Remove redundant code. # Make the interchangbility work between various algorithms at each step. def on_click(): - ''' + """ This function defines the action of the 'Next' button. - ''' + """ global algo, counter, next_button, romania_problem, start, goal romania_problem = GraphProblem(start.get(), goal.get(), romania_map) if "Breadth-First Tree Search" == algo.get(): @@ -546,8 +586,8 @@ def on_click(): display_final(final_path) next_button.config(state="disabled") counter += 1 - elif "Breadth-First Search" == algo.get(): - node = breadth_first_search(romania_problem) + elif "Breadth-First Graph Search" == algo.get(): + node = breadth_first_graph_search(romania_problem) if node is not None: final_path = bfs(romania_problem).solution() final_path.append(start.get()) @@ -605,7 +645,7 @@ def main(): algorithm_menu = OptionMenu( root, algo, "Breadth-First Tree Search", "Depth-First Tree Search", - "Breadth-First Search", "Depth-First Graph Search", + "Breadth-First Graph Search", "Depth-First Graph Search", "Uniform Cost Search", "A* - Search") Label(root, text="\n Search Algorithm").pack() algorithm_menu.pack() diff --git a/planning.py b/planning.py index c15172372..bb54f2027 100644 --- a/planning.py +++ b/planning.py @@ -3,8 +3,9 @@ import itertools from search import Node -from utils import Expr, expr, first, FIFOQueue +from utils import Expr, expr, first from logic import FolKB +from collections import deque class PDDL: @@ -727,16 +728,16 @@ def hierarchical_search(problem, hierarchy): """ [Figure 11.5] 'Hierarchical Search, a Breadth First Search implementation of Hierarchical Forward Planning Search' - The problem is a real-world prodlem defined by the problem class, and the hierarchy is + The problem is a real-world problem defined by the problem class, and the hierarchy is a dictionary of HLA - refinements (see refinements generator for details) """ act = Node(problem.actions[0]) - frontier = FIFOQueue() + frontier = deque() frontier.append(act) - while(True): + while True: if not frontier: return None - plan = frontier.pop() + plan = frontier.popleft() print(plan.state.name) hla = plan.state # first_or_null(plan) prefix = None diff --git a/search.py b/search.py index 7296429af..66b360335 100644 --- a/search.py +++ b/search.py @@ -6,11 +6,11 @@ from utils import ( is_in, argmin, argmax, argmax_random_tie, probability, weighted_sampler, - memoize, print_table, open_data, Stack, FIFOQueue, PriorityQueue, name, + memoize, print_table, open_data, PriorityQueue, name, distance, vector_add ) -from collections import defaultdict +from collections import defaultdict, deque import math import random import sys @@ -126,7 +126,7 @@ def path(self): node = node.parent return list(reversed(path_back)) - # We want for a queue of nodes in breadth_first_search or + # We want for a queue of nodes in breadth_first_graph_search or # astar_search to have no duplicated states, so we treat nodes # with the same state as equal. [Problem: this may not be what you # want in other contexts.] @@ -179,11 +179,30 @@ def search(self, problem): # Uninformed Search algorithms -def tree_search(problem, frontier): - """Search through the successors of a problem to find a goal. - The argument frontier should be an empty queue. - Repeats infinites in case of loops. [Figure 3.7]""" - frontier.append(Node(problem.initial)) +def breadth_first_tree_search(problem): + """Search the shallowest nodes in the search tree first. + Search through the successors of a problem to find a goal. + The argument frontier should be an empty queue. + Repeats infinitely in case of loops. [Figure 3.7]""" + + frontier = deque([Node(problem.initial)]) # FIFO queue + + while frontier: + node = frontier.popleft() + if problem.goal_test(node.state): + return node + frontier.extend(node.expand(problem)) + return None + + +def depth_first_tree_search(problem): + """Search the deepest nodes in the search tree first. + Search through the successors of a problem to find a goal. + The argument frontier should be an empty queue. + Repeats infinitely in case of loops. [Figure 3.7]""" + + frontier = [Node(problem.initial)] # Stack + while frontier: node = frontier.pop() if problem.goal_test(node.state): @@ -192,12 +211,13 @@ def tree_search(problem, frontier): return None -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)) +def depth_first_graph_search(problem): + """Search the deepest nodes in the search tree first. + 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 = [(Node(problem.initial))] # Stack explored = set() while frontier: node = frontier.pop() @@ -210,35 +230,19 @@ def graph_search(problem, frontier): return None -def breadth_first_tree_search(problem): - """Search the shallowest nodes in the search tree first.""" - return tree_search(problem, FIFOQueue()) - - -def depth_first_tree_search(problem): - """Search the deepest nodes in the search tree first.""" - return tree_search(problem, Stack()) - - -def depth_first_graph_search(problem): - """Search the deepest nodes in the search tree first.""" - return graph_search(problem, Stack()) - - -def breadth_first_search(problem): +def breadth_first_graph_search(problem): """[Figure 3.11] - Note that this function can be implemented in a - single line as below: - return graph_search(problem, FIFOQueue()) + 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 - frontier = FIFOQueue() - frontier.append(node) + frontier = deque([node]) explored = set() while frontier: - node = frontier.pop() + node = frontier.popleft() explored.add(node.state) for child in node.expand(problem): if child.state not in explored and child not in frontier: @@ -260,7 +264,7 @@ def best_first_graph_search(problem, f): node = Node(problem.initial) if problem.goal_test(node.state): return node - frontier = PriorityQueue(min, f) + frontier = PriorityQueue('min', f) frontier.append(node) explored = set() while frontier: @@ -470,10 +474,10 @@ def check_solvability(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): + if state[i] > state[j] != 0: inversion += 1 - return (inversion % 2 == 0) + return inversion % 2 == 0 def h(self, node): """ Return the heuristic value for a given state. Default heuristic function used is @@ -853,15 +857,13 @@ def recombine(x, y): def recombine_uniform(x, y): n = len(x) - result = [0] * n; + 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 + + return ''.join(str(r) for r in result) def mutate(x, gene_pool, pmut): @@ -1433,7 +1435,7 @@ def __repr__(self): def compare_searchers(problems, header, searchers=[breadth_first_tree_search, - breadth_first_search, + breadth_first_graph_search, depth_first_graph_search, iterative_deepening_search, depth_limited_search, diff --git a/tests/test_search.py b/tests/test_search.py index 3a9279c3e..0bdf65f44 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -16,11 +16,11 @@ def test_find_min_edge(): def test_breadth_first_tree_search(): assert breadth_first_tree_search( romania_problem).solution() == ['Sibiu', 'Fagaras', 'Bucharest'] - assert breadth_first_search(nqueens).solution() == [0, 4, 7, 5, 2, 6, 1, 3] + assert breadth_first_graph_search(nqueens).solution() == [0, 4, 7, 5, 2, 6, 1, 3] -def test_breadth_first_search(): - assert breadth_first_search(romania_problem).solution() == ['Sibiu', 'Fagaras', 'Bucharest'] +def test_breadth_first_graph_search(): + assert breadth_first_graph_search(romania_problem).solution() == ['Sibiu', 'Fagaras', 'Bucharest'] def test_best_first_graph_search(): @@ -333,7 +333,7 @@ def search(self, problem): >>> compare_graph_searchers() Searcher romania_map(A, B) romania_map(O, N) australia_map breadth_first_tree_search < 21/ 22/ 59/B> <1158/1159/3288/N> < 7/ 8/ 22/WA> - breadth_first_search < 7/ 11/ 18/B> < 19/ 20/ 45/N> < 2/ 6/ 8/WA> + breadth_first_graph_search < 7/ 11/ 18/B> < 19/ 20/ 45/N> < 2/ 6/ 8/WA> depth_first_graph_search < 8/ 9/ 20/B> < 16/ 17/ 38/N> < 4/ 5/ 11/WA> iterative_deepening_search < 11/ 33/ 31/B> < 656/1815/1812/N> < 3/ 11/ 11/WA> depth_limited_search < 54/ 65/ 185/B> < 387/1012/1125/N> < 50/ 54/ 200/WA> diff --git a/tests/test_utils.py b/tests/test_utils.py index dbc1bc01a..8c7f5c318 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -255,53 +255,6 @@ def test_expr(): assert (expr('GP(x, z) <== P(x, y) & P(y, z)') == Expr('<==', GP(x, z), P(x, y) & P(y, z))) -def test_FIFOQueue() : - # Create an object - queue = FIFOQueue() - # Generate an array of number to be used for testing - test_data = [ random.choice(range(100)) for i in range(100) ] - # Index of the element to be added in the queue - front_head = 0 - # Index of the element to be removed from the queue - back_head = 0 - while front_head < 100 or back_head < 100 : - if front_head == 100 : # only possible to remove - # check for pop and append method - assert queue.pop() == test_data[back_head] - back_head += 1 - elif back_head == front_head : # only possible to push element into queue - queue.append(test_data[front_head]) - front_head += 1 - # else do it in a random manner - elif random.random() < 0.5 : - assert queue.pop() == test_data[back_head] - back_head += 1 - else : - queue.append(test_data[front_head]) - front_head += 1 - # check for __len__ method - assert len(queue) == front_head - back_head - # check for __contains__ method - if front_head - back_head > 0 : - assert random.choice(test_data[back_head:front_head]) in queue - - # check extend method - test_data1 = [ random.choice(range(100)) for i in range(50) ] - test_data2 = [ random.choice(range(100)) for i in range(50) ] - # append elements of test data 1 - queue.extend(test_data1) - # append elements of test data 2 - queue.extend(test_data2) - # reset front_head - front_head = 0 - - while front_head < 50 : - assert test_data1[front_head] == queue.pop() - front_head += 1 - - while front_head < 100 : - assert test_data2[front_head - 50] == queue.pop() - front_head += 1 if __name__ == '__main__': pytest.main() diff --git a/utils.py b/utils.py index b0e57e41f..1ac0b13f7 100644 --- a/utils.py +++ b/utils.py @@ -3,6 +3,7 @@ import bisect import collections import collections.abc +import heapq import operator import os.path import random @@ -71,7 +72,7 @@ def mode(data): def powerset(iterable): """powerset([1,2,3]) --> (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)""" s = list(iterable) - return list(chain.from_iterable(combinations(s, r) for r in range(len(s)+1)))[1:] + return list(chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)))[1:] # ______________________________________________________________________________ @@ -193,7 +194,7 @@ def inverse_matrix(X): assert len(X[0]) == 2 det = X[0][0] * X[1][1] - X[0][1] * X[1][0] assert det != 0 - inv_mat = scalar_matrix_product(1.0/det, [[X[1][1], -X[0][1]], [-X[1][0], X[0][0]]]) + inv_mat = scalar_matrix_product(1.0 / det, [[X[1][1], -X[0][1]], [-X[1][0], X[0][0]]]) return inv_mat @@ -226,7 +227,7 @@ def rounder(numbers, d=4): if isinstance(numbers, (int, float)): return round(numbers, d) else: - constructor = type(numbers) # Can be list, set, tuple, etc. + constructor = type(numbers) # Can be list, set, tuple, etc. return constructor(rounder(n, d) for n in numbers) @@ -256,7 +257,7 @@ def normalize(dist): def norm(X, n=2): """Return the n-norm of vector X""" - return sum([x**n for x in X])**(1/n) + return sum([x ** n for x in X]) ** (1 / n) def clip(x, lowest, highest): @@ -270,7 +271,7 @@ def sigmoid_derivative(value): def sigmoid(x): """Return activation value of x with sigmoid function""" - return 1/(1 + math.exp(-x)) + return 1 / (1 + math.exp(-x)) def step(x): @@ -280,7 +281,7 @@ def step(x): def gaussian(mean, st_dev, x): """Given the mean and standard deviation of a distribution, it returns the probability of x.""" - return 1/(math.sqrt(2*math.pi)*st_dev)*math.e**(-0.5*(float(x-mean)/st_dev)**2) + return 1 / (math.sqrt(2 * math.pi) * st_dev) * math.e ** (-0.5 * (float(x - mean) / st_dev) ** 2) try: # math.isclose was added in Python 3.5; but we might be in 3.4 @@ -335,7 +336,7 @@ def distance_squared(a, b): """The square of the distance between two (x, y) points.""" xA, yA = a xB, yB = b - return (xA - xB)**2 + (yA - yB)**2 + return (xA - xB) ** 2 + (yA - yB) ** 2 def vector_clip(vector, lowest, highest): @@ -351,12 +352,15 @@ def vector_clip(vector, lowest, highest): class injection(): """Dependency injection of temporary values for global functions/classes/etc. E.g., `with injection(DataBase=MockDataBase): ...`""" - def __init__(self, **kwds): + + def __init__(self, **kwds): self.new = kwds - def __enter__(self): + + def __enter__(self): self.old = {v: globals()[v] for v in self.new} globals().update(self.new) - def __exit__(self, type, value, traceback): + + def __exit__(self, type, value, traceback): globals().update(self.old) @@ -412,8 +416,8 @@ def print_table(table, header=None, sep=' ', numfmt='{}'): for row in table] sizes = list( - map(lambda seq: max(map(len, seq)), - list(zip(*[map(str, row) for row in table])))) + map(lambda seq: max(map(len, seq)), + list(zip(*[map(str, row) for row in table])))) for row in table: print(sep.join(getattr( @@ -424,7 +428,7 @@ def open_data(name, mode='r'): aima_root = os.path.dirname(__file__) aima_file = os.path.join(aima_root, *['aima-data', name]) - return open(aima_file) + return open(aima_file, mode=mode) def failure_test(algorithm, tests): @@ -563,19 +567,21 @@ def __eq__(self, other): and self.op == other.op and self.args == other.args) - def __hash__(self): return hash(self.op) ^ hash(self.args) + def __hash__(self): + return hash(self.op) ^ hash(self.args) def __repr__(self): op = self.op args = [str(arg) for arg in self.args] - if op.isidentifier(): # f(x) or f(x, y) + if op.isidentifier(): # f(x) or f(x, y) return '{}({})'.format(op, ', '.join(args)) if args else op - elif len(args) == 1: # -x or -(x + 1) + elif len(args) == 1: # -x or -(x + 1) return op + args[0] - else: # (x - y) + else: # (x - y) opp = (' ' + op + ' ') return '(' + opp.join(args) + ')' + # An 'Expression' is either an Expr or a Number. # Symbol is not an explicit type; it is any Expr with 0 args. @@ -609,11 +615,13 @@ def arity(expression): else: # expression is a number return 0 + # For operators that are not defined in Python, we allow new InfixOps: class PartialExpr: """Given 'P |'==>'| Q, first form PartialExpr('==>', P), then combine with Q.""" + def __init__(self, op, lhs): self.op, self.lhs = op, lhs @@ -656,6 +664,7 @@ class defaultkeydict(collections.defaultdict): >>> d = defaultkeydict(len); d['four'] 4 """ + def __missing__(self, key): self[key] = result = self.default_factory(key) return result @@ -665,132 +674,68 @@ class hashabledict(dict): """Allows hashing by representing a dictionary as tuple of key:value pairs May cause problems as the hash value may change during runtime """ - def __tuplify__(self): - return tuple(sorted(self.items())) def __hash__(self): - return hash(self.__tuplify__()) - - def __lt__(self, odict): - assert isinstance(odict, hashabledict) - return self.__tuplify__() < odict.__tuplify__() - - def __gt__(self, odict): - assert isinstance(odict, hashabledict) - return self.__tuplify__() > odict.__tuplify__() - - def __le__(self, odict): - assert isinstance(odict, hashabledict) - return self.__tuplify__() <= odict.__tuplify__() - - def __ge__(self, odict): - assert isinstance(odict, hashabledict) - return self.__tuplify__() >= odict.__tuplify__() + return 1 # ______________________________________________________________________________ # Queues: Stack, FIFOQueue, PriorityQueue +# Stack and FIFOQueue are implemented as list and collection.deque +# PriorityQueue is implemented here -# TODO: queue.PriorityQueue -# TODO: Priority queues may not belong here -- see treatment in search.py - - -class Queue: - - """Queue is an abstract class/interface. There are three types: - Stack(): A Last In First Out Queue. - FIFOQueue(): A First In First Out Queue. - PriorityQueue(order, f): Queue in sorted order (default min-first). - Each type supports the following methods and functions: - q.append(item) -- add an item to the queue - q.extend(items) -- equivalent to: for item in items: q.append(item) - q.pop() -- return the top item from the queue - len(q) -- number of items in q (also q.__len()) - item in q -- does q contain item? - Note that isinstance(Stack(), Queue) is false, because we implement stacks - as lists. If Python ever gets interfaces, Queue will be an interface.""" - - def __init__(self): - raise NotImplementedError - - def extend(self, items): - for item in items: - self.append(item) - - -def Stack(): - """Return an empty list, suitable as a Last-In-First-Out Queue.""" - return [] +class PriorityQueue: + """A Queue in which the minimum (or maximum) element (as determined by f and + order) is returned first. + If order is 'min', the item with minimum f(x) is + returned first; if order is 'max', then it is the item with maximum f(x). + Also supports dict-like lookup.""" -class FIFOQueue(Queue): - - """A First-In-First-Out Queue.""" + def __init__(self, order='min', f=lambda x: x): + self.heap = [] - def __init__(self, maxlen=None, items=[]): - self.queue = collections.deque(items, maxlen) + if order == 'min': + self.f = f + elif order == 'max': # now item with max f(x) + self.f = lambda x: -f(x) # will be popped first + else: + raise ValueError("order must be either 'min' or max'.") def append(self, item): - if not self.queue.maxlen or len(self.queue) < self.queue.maxlen: - self.queue.append(item) - else: - raise Exception('FIFOQueue is full') + """Insert item at its correct position.""" + heapq.heappush(self.heap, (self.f(item), item)) def extend(self, items): - if not self.queue.maxlen or len(self.queue) + len(items) <= self.queue.maxlen: - self.queue.extend(items) - else: - raise Exception('FIFOQueue max length exceeded') + """Insert each item in items at its correct position.""" + for item in items: + self.heap.append(item) def pop(self): - if len(self.queue) > 0: - return self.queue.popleft() + """Pop and return the item (with min or max f(x) value + depending on the order.""" + if self.heap: + return heapq.heappop(self.heap)[1] else: - raise Exception('FIFOQueue is empty') + raise Exception('Trying to pop from empty PriorityQueue.') def __len__(self): - return len(self.queue) + """Return current capacity of PriorityQueue.""" + return len(self.heap) def __contains__(self, item): - return item in self.queue - - -class PriorityQueue(Queue): - - """A queue in which the minimum (or maximum) element (as determined by f and - order) is returned first. If order is min, the item with minimum f(x) is - returned first; if order is max, then it is the item with maximum f(x). - Also supports dict-like lookup.""" - - def __init__(self, order=min, f=lambda x: x): - self.A = [] - self.order = order - self.f = f - - def append(self, item): - bisect.insort(self.A, (self.f(item), item)) - - def __len__(self): - return len(self.A) - - def pop(self): - if self.order == min: - return self.A.pop(0)[1] - else: - return self.A.pop()[1] - - def __contains__(self, item): - return any(item == pair[1] for pair in self.A) + """Return True if item in PriorityQueue.""" + return (self.f(item), item) in self.heap def __getitem__(self, key): - for _, item in self.A: + for _, item in self.heap: if item == key: return item def __delitem__(self, key): - for i, (value, item) in enumerate(self.A): - if item == key: - self.A.pop(i) + """Delete the first occurrence of key.""" + self.heap.remove((self.f(key), key)) + heapq.heapify(self.heap) # ______________________________________________________________________________