def test_priority_queue(): """ Ensure the priority set is sorting elements correctly. """ random_elements = [i for i in range(10)] shuffle(random_elements) pq = PriorityQueue(node_value=lambda x: x) for e in random_elements: pq.push(e) output = [] while len(pq) > 0: output.append(pq.pop()) random_elements.sort() assert output == random_elements
def test_priority_queue_cost_limit(): """ Ensure the priority queue is enforcing a cost limit """ random_elements = [i for i in range(10)] shuffle(random_elements) pq = PriorityQueue(node_value=lambda x: x, cost_limit=3) for e in random_elements: pq.push(e) output = [] while len(pq) > 0: output.append(pq.pop()) random_elements.sort() assert output == random_elements[:4]
def beam_search(problem, beam_width=1, graph_search=True): """ A variant of breadth-first search where all nodes in the fringe are expanded, but the resulting new fringe is limited to have length beam_width, where the nodes with the worst value are dropped. The default beam width is 1, which yields greedy best-first search (i.e., hill climbing). There are different ways to implement beam search, namely best-first beam search and breadth-first beam search. According to: Wilt, C. M., Thayer, J. T., & Ruml, W. (2010). A comparison of greedy search algorithms. In Third Annual Symposium on Combinatorial Search. breadth-first beam search almost always performs better. They find that allowing the search to re-expand duplicate nodes if they have a lower cost improves search performance. Thus, our implementation is a breadth-first beam search that re-expand duplicate nodes with lower cost. :param problem: The problem to solve. :type problem: :class:`Problem` :param beam_width: The size of the beam (defaults to 1). :type beam_width: int :param graph_search: whether to use graph or tree search. :type graph_search: boolean """ closed = {} fringe = PriorityQueue(node_value=problem.node_value) fringe.push(problem.initial) closed[problem.initial] = problem.initial.cost() while len(fringe) > 0: parents = [] while len(fringe) > 0 and len(parents) < beam_width: parent = fringe.pop() if problem.goal_test(parent): yield parent parents.append(parent) fringe.clear() for node in parents: for s in problem.successors(node): if not graph_search: fringe.push(s) elif s not in closed or s.cost() < closed[s]: fringe.push(s) closed[s] = s.cost()
def beam_search(problem, beam_width=1, graph=True): """ A variant of breadth-first search where all nodes in the fringe are expanded, but the resulting new fringe is limited to have length beam_width, where the nodes with the worst value are dropped. The default beam width is 1, which yields greedy best-first search (i.e., hill climbing). There are different ways to implement beam search, namely best-first beam search and breadth-first beam search. According to: Wilt, C. M., Thayer, J. T., & Ruml, W. (2010). A comparison of greedy search algorithms. In Third Annual Symposium on Combinatorial Search. breadth-first beam search almost always performs better. They find that allowing the search to re-expand duplicate nodes if they have a lower cost improves search performance. Thus, our implementation is a breadth-first beam search that re-expand duplicate nodes with lower cost. :param problem: The problem to solve. :type problem: :class:`Problem` :param beam_width: The size of the beam (defaults to 1). :type beam_width: int :param graph_search: whether to use graph or tree search. :type graph_search: boolean """ closed = {} fringe = PriorityQueue(node_value=problem.node_value) fringe.push(problem.initial) closed[problem.initial] = problem.initial.cost() while len(fringe) > 0: parents = [] while len(fringe) > 0 and len(parents) < beam_width: parent = fringe.pop() if problem.goal_test(parent, problem.goal): yield SolutionNode(parent, problem.goal) parents.append(parent) fringe.clear() for node in parents: for s in problem.successors(node): if not graph: fringe.push(s) elif s not in closed or s.cost() < closed[s]: fringe.push(s) closed[s] = s.cost()
def local_beam_search(problem, beam_width=1, max_sideways=0, graph_search=True, cost_limit=float('-inf')): """ A variant of :func:`py_search.informed_search.beam_search` that can be applied to local search problems. When the beam width of 1 this approach yields behavior similar to :func:`hill_climbing`. :param problem: The problem to solve. :type problem: :class:`py_search.base.Problem` :param beam_width: The size of the search beam. :type beam_width: int :param max_sideways: Specifies the max number of contiguous sideways moves. :type max_sideways: int :param graph_search: Whether to use graph search (no duplicates) or tree search (duplicates) :type graph_search: Boolean """ b = None bv = float('inf') sideways_moves = 0 fringe = PriorityQueue(node_value=problem.node_value) fringe.push(problem.initial) while len(fringe) < beam_width: fringe.push(problem.random_node()) if graph_search: closed = set() closed.add(problem.initial) while len(fringe) > 0 and sideways_moves <= max_sideways: pv = fringe.peek_value() if pv > bv: yield b if pv == bv: sideways_moves += 1 else: sideways_moves = 0 parents = [] while len(fringe) > 0 and len(parents) < beam_width: parent = fringe.pop() parents.append(parent) fringe.clear() b = parents[0] bv = pv for node in parents: for s in problem.successors(node): added = True if not graph_search: fringe.push(s) elif s not in closed: fringe.push(s) closed.add(s) else: added = False if added and fringe.peek_value() <= cost_limit: yield fringe.peek() yield b
def test_priority_queue(): """ Ensure the priority set is sorting elements correctly. """ random_elements = [i for i in range(10)] shuffle(random_elements) pq = PriorityQueue() for e in random_elements: pq.push(e) random_elements.sort() assert [e for e in pq] == random_elements assert pq.peek() == random_elements[0] output = [] while len(pq) > 0: output.append(pq.pop()) assert output == random_elements for e in random_elements: pq.push(e) assert len(pq) == 10 pq.update_cost_limit(5) assert len(pq) == 6 output = [] while len(pq) > 0: output.append(pq.pop()) assert output == random_elements[:6] pq = PriorityQueue(node_value=lambda x: x, max_length=3) pq.push(6) pq.push(0) pq.push(2) pq.push(6) pq.push(7) assert len(pq) == 3 assert list(pq) == [0, 2, 6] pq.update_cost_limit(5) assert len(pq) == 2 assert pq.peek() == 0 assert pq.peek_value() == 0 assert pq.pop() == 0 assert pq.peek() == 2 assert pq.peek_value() == 2 assert pq.pop() == 2 assert len(pq) == 0
def branch_and_bound(problem, graph=True, depth_limit=float('inf')): """ An exhaustive optimization technique that is guranteed to give the best solution. In general the algorithm starts with some (potentially non-optimal) solution. Then it uses the cost of the current best solution to prune branches of the search that do not have any chance of being better than this solution (i.e., that have a node_value > current best cost). In this implementation, node_value should provide an admissible lower bound on the cost of solutions reachable from the provided node. If node_value is inadmissible, then optimality guarantees are lost. Also, if the search space is infinite and/or the node_value function provides too little guidance (e.g., node_value = float('-inf')), then the search might never terminate. To counter this, a depth_limit can be provided that stops expanding nodes after the provided depth. This will ensure the search is finite and guaranteed to terminate. Finally, the problem.goal_test function can be used to terminate search early if a good enough solution has been found. If goal_test(node) return True, then search is immediately terminated and the node is returned. Note, the current implementation uses best-first search via a priority queue data structure. :param problem: The problem to solve. :type problem: :class:`py_search.base.Problem` :param graph: Whether to use graph search (no duplicates) or tree search (duplicates). :type graph: Boolean """ b = None bv = float('inf') fringe = PriorityQueue(node_value=problem.node_value) fringe.push(problem.initial) if graph: closed = set() closed.add(problem.initial) while len(fringe) > 0: pv = fringe.peek_value() if bv < pv: break node = fringe.pop() if problem.goal_test(node, problem.goal): yield SolutionNode(node, problem.goal) if problem.node_value(node) < bv: b = node bv = problem.node_value(node) fringe.update_cost_limit(bv) if depth_limit == float('inf') or node.depth() < depth_limit: for s in problem.successors(node): if not graph: fringe.push(s) elif s not in closed: fringe.push(s) closed.add(s) yield SolutionNode(b, problem.goal)
def local_beam_search(problem, beam_width=1, max_sideways=0, graph=True): """ A variant of :func:`py_search.informed_search.beam_search` that can be applied to local search problems. When the beam width of 1 this approach yields behavior similar to :func:`hill_climbing`. The problem.goal_test function can be used to terminate search early if a good enough solution has been found. If goal_test(node) return True, then search is immediately terminated and the node is returned. :param problem: The problem to solve. :type problem: :class:`py_search.base.Problem` :param beam_width: The size of the search beam. :type beam_width: int :param max_sideways: Specifies the max number of contiguous sideways moves. :type max_sideways: int :param graph: Whether to use graph search (no duplicates) or tree search (duplicates) :type graph: Boolean """ b = None bv = float('inf') sideways_moves = 0 fringe = PriorityQueue(node_value=problem.node_value) fringe.push(problem.initial) while len(fringe) < beam_width: fringe.push(problem.random_node()) if graph: closed = set() closed.add(problem.initial) while len(fringe) > 0 and sideways_moves <= max_sideways: pv = fringe.peek_value() if bv < pv: yield SolutionNode(b, problem.goal) if pv == bv: sideways_moves += 1 else: sideways_moves = 0 parents = [] while len(fringe) > 0 and len(parents) < beam_width: parent = fringe.pop() parents.append(parent) fringe.clear() b = parents[0] bv = pv for node in parents: if problem.goal_test(node, problem.goal): yield SolutionNode(node, problem.goal) for s in problem.successors(node): if not graph: fringe.push(s) elif s not in closed: fringe.push(s) closed.add(s) yield SolutionNode(b, problem.goal)