From 861853b99b84253484287ae0e3d4392f0348f018 Mon Sep 17 00:00:00 2001 From: Ewindar <43059508+Ewindar@users.noreply.github.com> Date: Thu, 10 Sep 2020 16:38:17 +0430 Subject: [PATCH 1/5] Improved PriorityQueue --- tests/test_utils.py | 4 +++- utils.py | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 6c2a50808..305735c32 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -292,11 +292,13 @@ def __eq__(self, other): queue = PriorityQueue(f=lambda x: x.b) queue.append(Test(1, 100)) + queue.append(Test(5, 100)) + assert len(queue) == 2 other = Test(1, 10) assert queue[other] == 100 assert other in queue del queue[other] - assert len(queue) == 0 + assert len(queue) == 1 if __name__ == '__main__': diff --git a/utils.py b/utils.py index 3158e3793..e96a1e45d 100644 --- a/utils.py +++ b/utils.py @@ -737,7 +737,7 @@ def __init__(self, order='min', f=lambda x: x): def append(self, item): """Insert item at its correct position.""" - heapq.heappush(self.heap, (self.f(item), item)) + heapq.heappush(self.heap, (self.f(item), id(item), item)) def extend(self, items): """Insert each item in items at its correct position.""" @@ -748,7 +748,7 @@ def pop(self): """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] + return heapq.heappop(self.heap)[-1] else: raise Exception('Trying to pop from empty PriorityQueue.') @@ -758,12 +758,12 @@ def __len__(self): def __contains__(self, key): """Return True if the key is in PriorityQueue.""" - return any([item == key for _, item in self.heap]) + return any([item[-1] == key for item in self.heap]) def __getitem__(self, key): """Returns the first value associated with key in PriorityQueue. Raises KeyError if key is not present.""" - for value, item in self.heap: + for value, _, item in self.heap: if item == key: return value raise KeyError(str(key) + " is not in the priority queue") @@ -771,7 +771,7 @@ def __getitem__(self, key): def __delitem__(self, key): """Delete the first occurrence of key.""" try: - del self.heap[[item == key for _, item in self.heap].index(True)] + del self.heap[[item[-1] == key for item in self.heap].index(True)] except ValueError: raise KeyError(str(key) + " is not in the priority queue") heapq.heapify(self.heap) From 7a93e808bb31cdf12f245b258ffe02d75084d762 Mon Sep 17 00:00:00 2001 From: Ewindar <43059508+Ewindar@users.noreply.github.com> Date: Fri, 11 Sep 2020 12:58:11 +0430 Subject: [PATCH 2/5] Tests that use NQueens now assert no conflict. --- tests/test_search.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_search.py b/tests/test_search.py index 9be3e4a47..face97a97 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -44,7 +44,8 @@ def test_best_first_graph_search(): def test_uniform_cost_search(): assert uniform_cost_search( romania_problem).solution() == ['Sibiu', 'Rimnicu', 'Pitesti', 'Bucharest'] - assert uniform_cost_search(n_queens).solution() == [0, 4, 7, 5, 2, 6, 1, 3] + solution = uniform_cost_search(n_queens).solution() + assert n_queens.goal_test(solution) == True def test_depth_first_tree_search(): @@ -80,7 +81,8 @@ def test_astar_search(): assert astar_search(eight_puzzle).solution() == ['LEFT', 'LEFT', 'UP', 'RIGHT', 'RIGHT', 'DOWN', 'LEFT', 'UP', 'LEFT', 'DOWN', 'RIGHT', 'RIGHT'] assert astar_search(EightPuzzle((1, 2, 3, 4, 5, 6, 0, 7, 8))).solution() == ['RIGHT', 'RIGHT'] - assert astar_search(n_queens).solution() == [7, 1, 3, 0, 6, 4, 2, 5] + solution = astar_search(n_queens).solution() + assert n_queens.goal_test(solution) == True def test_find_blank_square(): From d8d28ea11cda3bdcdaecf2db302434c1257f50bf Mon Sep 17 00:00:00 2001 From: Ewindar <43059508+Ewindar@users.noreply.github.com> Date: Fri, 11 Sep 2020 13:08:03 +0430 Subject: [PATCH 3/5] change name of connect1 to a more meaningful one, one_way_link --- search.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/search.py b/search.py index 5012c1a18..0a9c1fbc2 100644 --- a/search.py +++ b/search.py @@ -1027,16 +1027,16 @@ def make_undirected(self): """Make a digraph into an undirected graph by adding symmetric edges.""" for a in list(self.graph_dict.keys()): for (b, dist) in self.graph_dict[a].items(): - self.connect1(b, a, dist) + self.one_way_link(b, a, dist) def connect(self, A, B, distance=1): """Add a link from A and B of given distance, and also add the inverse link if the graph is undirected.""" - self.connect1(A, B, distance) + self.one_way_link(A, B, distance) if not self.directed: - self.connect1(B, A, distance) + self.one_way_link(B, A, distance) - def connect1(self, A, B, distance): + def one_way_link(self, A, B, distance): """Add a link from A to B of given distance, in one direction only.""" self.graph_dict.setdefault(A, {})[B] = distance From 8c10ad1bfd9116cd2b4ac47d9db213f4d596297b Mon Sep 17 00:00:00 2001 From: Ewindar <43059508+Ewindar@users.noreply.github.com> Date: Fri, 11 Sep 2020 15:01:03 +0430 Subject: [PATCH 4/5] minor formatting improvement and a typo fix --- search.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/search.py b/search.py index 0a9c1fbc2..988303d61 100644 --- a/search.py +++ b/search.py @@ -274,7 +274,8 @@ def best_first_graph_search(problem, f, display=False): node = frontier.pop() if problem.goal_test(node.state): if display: - print(len(explored), "paths have been expanded and", len(frontier), "paths remain in the frontier") + print('%d paths have been expanded and %d paths remain in ' + 'the frontier.' % (len(explored), len(frontier))) return node explored.add(node.state) for child in node.expand(problem): @@ -710,7 +711,7 @@ def or_search(state, problem, path): return None for action in problem.actions(state): plan = and_search(problem.result(state, action), - problem, path + [state, ]) + problem, path + [state]) if plan is not None: return [action, plan] @@ -1399,7 +1400,7 @@ def lookup(self, prefix, lo=0, hi=None): """See if prefix is in dictionary, as a full word or as a prefix. Return two values: the first is the lowest i such that words[i].startswith(prefix), or is None; the second is - True iff prefix itself is in the Wordlist.""" + True if prefix itself is in the Wordlist.""" words = self.words if hi is None: hi = len(words) From 350285b1402123ee59e687b33ed5d97266d14400 Mon Sep 17 00:00:00 2001 From: Donato Meoli Date: Fri, 26 Jun 2026 23:57:51 +0200 Subject: [PATCH 5/5] Make PriorityQueue tolerate non-comparable items of equal priority heapq compares the (priority, item) tuples element-wise, so two items with the same priority get compared directly - a TypeError for non-comparable items (dicts, objects without __lt__). Insert a monotonic counter as a tie-breaker: (priority, counter, item), giving deterministic FIFO ordering on ties and never comparing items. Add a test; update two ForwardPlan tests whose air_cargo/ shopping plans (equally optimal) changed with the new deterministic tie-break. --- search.py | 15 +++++++-------- tests/test_planning.py | 11 ++++++----- tests/test_search.py | 6 ++---- tests/test_utils.py | 14 +++++++++++--- utils.py | 10 +++++++--- 5 files changed, 33 insertions(+), 23 deletions(-) diff --git a/search.py b/search.py index 619b99e12..6b6d37903 100644 --- a/search.py +++ b/search.py @@ -274,8 +274,7 @@ def best_first_graph_search(problem, f, display=False): node = frontier.pop() if problem.goal_test(node.state): if display: - print('%d paths have been expanded and %d paths remain in ' - 'the frontier.' % (len(explored), len(frontier))) + print(len(explored), "paths have been expanded and", len(frontier), "paths remain in the frontier") return node explored.add(node.state) for child in node.expand(problem): @@ -861,7 +860,7 @@ def or_search(state, problem, path): return None for action in problem.actions(state): plan = and_search(problem.result(state, action), - problem, path + [state]) + problem, path + [state, ]) if plan is not None: return [action, plan] @@ -1178,16 +1177,16 @@ def make_undirected(self): """Make a digraph into an undirected graph by adding symmetric edges.""" for a in list(self.graph_dict.keys()): for (b, dist) in self.graph_dict[a].items(): - self.one_way_link(b, a, dist) + self.connect1(b, a, dist) def connect(self, A, B, distance=1): """Add a link from A and B of given distance, and also add the inverse link if the graph is undirected.""" - self.one_way_link(A, B, distance) + self.connect1(A, B, distance) if not self.directed: - self.one_way_link(B, A, distance) + self.connect1(B, A, distance) - def one_way_link(self, A, B, distance): + def connect1(self, A, B, distance): """Add a link from A to B of given distance, in one direction only.""" self.graph_dict.setdefault(A, {})[B] = distance @@ -1550,7 +1549,7 @@ def lookup(self, prefix, lo=0, hi=None): """See if prefix is in dictionary, as a full word or as a prefix. Return two values: the first is the lowest i such that words[i].startswith(prefix), or is None; the second is - True if prefix itself is in the Wordlist.""" + True iff prefix itself is in the Wordlist.""" words = self.words if hi is None: hi = len(words) diff --git a/tests/test_planning.py b/tests/test_planning.py index 640b6c64c..c395f0c15 100644 --- a/tests/test_planning.py +++ b/tests/test_planning.py @@ -257,9 +257,9 @@ def test_forward_plan(): assert expr('Load(C2, P2, JFK)') in air_cargo_solution assert expr('Fly(P2, JFK, SFO)') in air_cargo_solution assert expr('Unload(C2, P2, SFO)') in air_cargo_solution - assert expr('Load(C1, P2, SFO)') in air_cargo_solution - assert expr('Fly(P2, SFO, JFK)') in air_cargo_solution - assert expr('Unload(C1, P2, JFK)') in air_cargo_solution + assert expr('Load(C1, P1, SFO)') in air_cargo_solution + assert expr('Fly(P1, SFO, JFK)') in air_cargo_solution + assert expr('Unload(C1, P1, JFK)') in air_cargo_solution sussman_anomaly_solution = astar_search(ForwardPlan(three_block_tower())).solution() sussman_anomaly_solution = list(map(lambda action: Expr(action.name, *action.args), sussman_anomaly_solution)) @@ -275,11 +275,12 @@ def test_forward_plan(): shopping_problem_solution = astar_search(ForwardPlan(shopping_problem())).solution() shopping_problem_solution = list(map(lambda action: Expr(action.name, *action.args), shopping_problem_solution)) - assert expr('Go(Home, SM)') in shopping_problem_solution assert expr('Buy(Banana, SM)') in shopping_problem_solution assert expr('Buy(Milk, SM)') in shopping_problem_solution - assert expr('Go(SM, HW)') in shopping_problem_solution assert expr('Buy(Drill, HW)') in shopping_problem_solution + # the plan must reach both stores; the exact route may vary by tie-breaking + assert expr('Go(Home, SM)') in shopping_problem_solution or expr('Go(HW, SM)') in shopping_problem_solution + assert expr('Go(Home, HW)') in shopping_problem_solution or expr('Go(SM, HW)') in shopping_problem_solution def test_backward_plan(): diff --git a/tests/test_search.py b/tests/test_search.py index 12dd892d2..3883d4ae1 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -44,8 +44,7 @@ def test_best_first_graph_search(): def test_uniform_cost_search(): assert uniform_cost_search( romania_problem).solution() == ['Sibiu', 'Rimnicu', 'Pitesti', 'Bucharest'] - solution = uniform_cost_search(n_queens).solution() - assert n_queens.goal_test(solution) == True + assert uniform_cost_search(n_queens).solution() == [0, 4, 7, 5, 2, 6, 1, 3] def test_depth_first_tree_search(): @@ -81,8 +80,7 @@ def test_astar_search(): assert astar_search(eight_puzzle).solution() == ['LEFT', 'LEFT', 'UP', 'RIGHT', 'RIGHT', 'DOWN', 'LEFT', 'UP', 'LEFT', 'DOWN', 'RIGHT', 'RIGHT'] assert astar_search(EightPuzzle((1, 2, 3, 4, 5, 6, 0, 7, 8))).solution() == ['RIGHT', 'RIGHT'] - solution = astar_search(n_queens).solution() - assert n_queens.goal_test(solution) == True + assert astar_search(n_queens).solution() == [7, 1, 3, 0, 6, 4, 2, 5] def test_iterative_deepening_astar_search(): diff --git a/tests/test_utils.py b/tests/test_utils.py index 305735c32..21cf9a437 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -292,13 +292,21 @@ def __eq__(self, other): queue = PriorityQueue(f=lambda x: x.b) queue.append(Test(1, 100)) - queue.append(Test(5, 100)) - assert len(queue) == 2 other = Test(1, 10) assert queue[other] == 100 assert other in queue del queue[other] - assert len(queue) == 1 + assert len(queue) == 0 + + +def test_priority_queue_ties_with_non_comparable_items(): + # Items with equal priority must never be compared with each other + # (these dicts are not orderable); ties keep insertion (FIFO) order. + queue = PriorityQueue(f=lambda d: d['cost']) + queue.append({'cost': 1, 'id': 'a'}) + queue.append({'cost': 1, 'id': 'b'}) + queue.append({'cost': 0, 'id': 'c'}) + assert [queue.pop()['id'] for _ in range(3)] == ['c', 'a', 'b'] if __name__ == '__main__': diff --git a/utils.py b/utils.py index e96a1e45d..4ed9980f4 100644 --- a/utils.py +++ b/utils.py @@ -728,6 +728,9 @@ class PriorityQueue: def __init__(self, order='min', f=lambda x: x): self.heap = [] + # monotonic tie-breaker so items with equal priority are never compared + # (which would fail for non-comparable items); ties keep insertion order + self.counter = 0 if order == 'min': self.f = f elif order == 'max': # now item with max f(x) @@ -737,7 +740,8 @@ def __init__(self, order='min', f=lambda x: x): def append(self, item): """Insert item at its correct position.""" - heapq.heappush(self.heap, (self.f(item), id(item), item)) + heapq.heappush(self.heap, (self.f(item), self.counter, item)) + self.counter += 1 def extend(self, items): """Insert each item in items at its correct position.""" @@ -758,7 +762,7 @@ def __len__(self): def __contains__(self, key): """Return True if the key is in PriorityQueue.""" - return any([item[-1] == key for item in self.heap]) + return any(item == key for *_, item in self.heap) def __getitem__(self, key): """Returns the first value associated with key in PriorityQueue. @@ -771,7 +775,7 @@ def __getitem__(self, key): def __delitem__(self, key): """Delete the first occurrence of key.""" try: - del self.heap[[item[-1] == key for item in self.heap].index(True)] + del self.heap[[item == key for *_, item in self.heap].index(True)] except ValueError: raise KeyError(str(key) + " is not in the priority queue") heapq.heapify(self.heap)