def breadth_first_search(planning_task): """ Searches for a plan on the given task using breadth first search and duplicate detection. @param planning_task: The planning task to solve. @return: The solution as a list of operators or None if the task is unsolvable. """ # counts the number of loops (only for printing) iteration = 0 # fifo-queue storing the nodes which are next to explore queue = deque() queue.append(searchspace.make_root_node(planning_task.initial_state)) # set storing the explored nodes, used for duplicate detection closed = {planning_task.initial_state} while queue: iteration += 1 logging.debug("breadth_first_search: Iteration %d, #unexplored=%d" % (iteration, len(queue))) # get the next node to explore node = queue.popleft() # exploring the node or if it is a goal node extracting the plan if planning_task.goal_reached(node.state): logging.info("Goal reached. Start extraction of solution.") logging.info("%d Nodes expanded" % iteration) return node.extract_solution() for operator, successor_state in planning_task.get_successor_states( node.state): # duplicate detection if successor_state not in closed: queue.append( searchspace.make_child_node(node, operator, successor_state)) # remember the successor state closed.add(successor_state) logging.info("No operators left. Task unsolvable.") logging.info("%d Nodes expanded" % iteration) return None
""" Unit Testing for the search space module """ from pyperplan.search.searchspace import make_child_node, make_root_node # Construct a small tree in order to perform some needed test methods root = make_root_node("state1") child1 = make_child_node(root, "action1", "state2") child2 = make_child_node(root, "action2", "state3") grandchild1 = make_child_node(child1, "action3", "state4") grandchild2 = make_child_node(child2, "action4", "state5") def test_extract_solution(): """ Tests whether extract_solution method within class searchspace returns the list of actions starting from the root """ assert root.extract_solution() == [] assert grandchild1.extract_solution() == ["action1", "action3"] assert grandchild2.extract_solution() == ["action2", "action4"] def test_g_values(): """ Tests whether the distance of the node from the root is computed properly """ assert root.g == 0 assert child1.g == 1
finisher1-stack-letter sheet1 dummy-sheet """ optimal_plan = [op.strip() for op in optimal_plan.splitlines()] import os from pyperplan import planner from pyperplan.search import breadth_first_search, searchspace from pyperplan.task import Operator, Task benchmarks = os.path.abspath( os.path.join(os.path.abspath(__file__), "../../../../benchmarks")) # Collect problem files problem_file = os.path.join(benchmarks, "parcprinter", "task01.pddl") domain_file = planner.find_domain(problem_file) problem = planner._parse(domain_file, problem_file) task = planner._ground(problem) # Manually do the "search" node = searchspace.make_root_node(task.initial_state) for step, op_name in enumerate(optimal_plan, start=1): for op, successor_state in task.get_successor_states(node.state): if not op.name.strip("()") == op_name: continue node = searchspace.make_child_node(node, op, successor_state) # Check that we reached the goal assert len(task.goals - node.state) == 0
def astar_search( task, heuristic, make_open_entry=ordered_node_astar, use_relaxed_plan=False, max_search_time=float("inf"), ): """ Searches for a plan in the given task using A* search. @param task The task to be solved @param heuristic A heuristic callable which computes the estimated steps from a search node to reach the goal. @param make_open_entry An optional parameter to change the bahavior of the astar search. The callable should return a search node, possible values are ordered_node_astar, ordered_node_weighted_astar and ordered_node_greedy_best_first with obvious meanings. @param max_search_time Maximum search time in seconds """ open = [] state_cost = {task.initial_state: 0} node_tiebreaker = 0 root = searchspace.make_root_node(task.initial_state) init_h = heuristic(root) heapq.heappush(open, make_open_entry(root, init_h, node_tiebreaker)) _log.info("Initial h value: %f" % init_h) besth = float("inf") counter = 0 expansions = 0 # Number of heuristic calls, include the initial call for root note heuristic_calls = 1 # Used so we can interrupt the search start_time = time.perf_counter() while open: # Check whether max search time exceeded elapsed_time = time.perf_counter() - start_time if elapsed_time >= max_search_time: metrics = SearchMetrics( nodes_expanded=expansions, plan_length=-1, heuristic_calls=heuristic_calls, heuristic_val_for_initial_state=init_h, search_time=elapsed_time, search_state=SearchState.timed_out, ) _log.warning("Search timed out") _log.info("%d Nodes expanded" % expansions) _log.info("%d times heuristic called" % heuristic_calls) return [], metrics (f, h, _tie, pop_node) = heapq.heappop(open) if h < besth: besth = h _log.debug("Found new best h: %d after %d expansions" % (besth, counter)) pop_state = pop_node.state # Only expand the node if its associated cost (g value) is the lowest # cost known for this state. Otherwise we already found a cheaper # path after creating this node and hence can disregard it. if state_cost[pop_state] == pop_node.g: expansions += 1 if task.goal_reached(pop_state): _log.info("Goal reached. Start extraction of solution.") _log.info("%d Nodes expanded" % expansions) _log.info("%d times heuristic called" % heuristic_calls) sol = pop_node.extract_solution() # Create metrics metrics = SearchMetrics( nodes_expanded=expansions, plan_length=len(sol), heuristic_calls=heuristic_calls, heuristic_val_for_initial_state=init_h, search_time=time.perf_counter() - start_time, search_state=SearchState.success, ) return sol, metrics rplan = None if use_relaxed_plan: (rh, rplan) = heuristic.calc_h_with_plan( searchspace.make_root_node(pop_state)) _log.debug("relaxed plan %s " % rplan) for op, succ_state in task.get_successor_states(pop_state): if use_relaxed_plan: if rplan and not op.name in rplan: # ignore this operator if we use the relaxed plan # criterion _log.debug("removing operator %s << not a " "preferred operator" % op.name) continue else: _log.debug("keeping operator %s" % op.name) succ_node = searchspace.make_child_node( pop_node, op, succ_state) old_succ_g = state_cost.get(succ_state, float("inf")) if succ_node.g < old_succ_g: h = heuristic(succ_node) heuristic_calls += 1 if h == float("inf"): # don't bother with states that can't reach the goal anyway continue # We either never saw succ_state before, or we found a # cheaper path to succ_state than previously. node_tiebreaker += 1 heapq.heappush( open, make_open_entry(succ_node, h, node_tiebreaker)) state_cost[succ_state] = succ_node.g counter += 1 _log.info("No operators left. Task unsolvable.") _log.info("%d Nodes expanded" % expansions) # Create metrics metrics = SearchMetrics( nodes_expanded=expansions, plan_length=-1, heuristic_calls=heuristic_calls, heuristic_val_for_initial_state=init_h, search_time=time.perf_counter() - start_time, search_state=SearchState.failed, ) return None, metrics
def enforced_hillclimbing_search(planning_task, heuristic, use_preferred_ops=False): """ Searches for a plan on the given task using enforced hill climbing and duplicate detection. @param planning_task: The planning task to solve. @return: The solution as a list of operators or None if no solution was found. Note that enforced hill climbing is an incomplete algorith, so it may fail to find a solution even though the task is solvable. """ # counts the number of loops (only for printing) iteration = 0 # fifo-queue storing the nodes which are next to explore queue = deque() initial_node = searchspace.make_root_node(planning_task.initial_state) queue.append(initial_node) best_heuristic_value = heuristic(initial_node) logging.info("Initial h value: %f" % best_heuristic_value) # set storing the explored nodes, used for duplicate detection closed = set() visited = set() while queue: iteration += 1 # get the next node to explore node = queue.popleft() # remember the successor state closed.add(node.state) visited.add(node.state) # exploring the node or if it is a goal node extracting the plan if planning_task.goal_reached(node.state): logging.info("Goal reached. Start extraction of solution.") logging.info("%d Nodes expanded" % len(visited)) return node.extract_solution() # for the preferred operator version --> recompute heuristic and # relaxed plan if use_preferred_ops: (rh, rplan) = heuristic.calc_h_with_plan(node) logging.debug("relaxed plan %s " % rplan) for operator, successor_state in planning_task.get_successor_states( node.state): # for the preferred operator version ignore all non preferred # operators if use_preferred_ops: if rplan and not operator.name in rplan: # ignore this operator if we use the relaxed plan criterion logging.debug("removing operator %s << not a preferred " "operator" % operator.name) continue else: logging.debug("keeping operator %s" % operator.name) # duplicate detection if successor_state not in closed: successor_node = searchspace.make_child_node( node, operator, successor_state) heuristic_value = heuristic(successor_node) if heuristic_value == float("inf"): continue elif heuristic_value < best_heuristic_value: # Just take the first successor node that has a lower # heuristic value than the current best_heuristic_value # and ignore the other successor nodes. logging.debug("Found new best h: %f after %d expansions" % (heuristic_value, iteration)) queue.clear() closed.clear() best_heuristic_value = heuristic_value queue.append(successor_node) break else: queue.append(successor_node) logging.info("Enforced hill climbing failed") logging.info("%d Nodes expanded" % len(visited)) return None