Example #1
0
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
Example #2
0
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]
Example #3
0
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]
Example #4
0
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
Example #5
0
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()
Example #6
0
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()
Example #7
0
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
Example #8
0
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
Example #9
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)
Example #10
0
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)