@@ -1512,6 +1512,11 @@ class PartialOrderPlanner:
15121512
15131513 def __init__ (self , planning_problem ):
15141514 self .tries = 1
1515+ # safety bounds for the backtracking search in execute(): the maximum
1516+ # number of actions a plan may contain (iterative-deepening target) and
1517+ # the maximum number of node expansions per deepening level
1518+ self ._max_plan_actions = 12
1519+ self ._max_expansions = 20000
15151520 self .planning_problem = planning_problem
15161521 self .causal_links = []
15171522 self .start = Action ('Start' , [], self .planning_problem .initial )
@@ -1527,39 +1532,33 @@ def __init__(self, planning_problem):
15271532 self .expanded_actions = planning_problem .expand_actions ()
15281533
15291534 def find_open_precondition (self ):
1530- """Find open precondition with the least number of possible actions"""
1531-
1535+ """
1536+ Find the open precondition with the least number of achieving actions
1537+ (a most-constrained-variable heuristic). Returns the triple
1538+ (precondition, action_that_needs_it, [achieving_actions]). Iteration is
1539+ ordered deterministically so the search does not depend on set/hash
1540+ ordering. Returns (None, None, None) when some open precondition has no
1541+ achiever at all, which is a dead end for the current partial plan.
1542+ """
1543+ possible_actions = list (self .actions ) + self .expanded_actions
15321544 number_of_ways = dict ()
15331545 actions_for_precondition = dict ()
1534- for element in self .agenda :
1535- open_precondition = element [0 ]
1536- possible_actions = list (self .actions ) + self .expanded_actions
1537- for action in possible_actions :
1538- for effect in action .effect :
1539- if effect == open_precondition :
1540- if open_precondition in number_of_ways :
1541- number_of_ways [open_precondition ] += 1
1542- actions_for_precondition [open_precondition ].append (action )
1543- else :
1544- number_of_ways [open_precondition ] = 1
1545- actions_for_precondition [open_precondition ] = [action ]
1546-
1547- number = sorted (number_of_ways , key = number_of_ways .__getitem__ )
1548-
1549- for k , v in number_of_ways .items ():
1550- if v == 0 :
1546+ for open_precondition , act in sorted (self .agenda , key = str ):
1547+ if open_precondition in number_of_ways :
1548+ continue
1549+ achievers = [action for action in possible_actions
1550+ if any (effect == open_precondition for effect in action .effect )]
1551+ if not achievers :
15511552 return None , None , None
1553+ number_of_ways [open_precondition ] = len (achievers )
1554+ actions_for_precondition [open_precondition ] = achievers
15521555
1553- act1 = None
1554- for element in self .agenda :
1555- if element [0 ] == number [0 ]:
1556- act1 = element [1 ]
1557- break
1556+ if not number_of_ways :
1557+ return None , None , None
15581558
1559- if number [0 ] in self .expanded_actions :
1560- self .expanded_actions .remove (number [0 ])
1561-
1562- return number [0 ], act1 , actions_for_precondition [number [0 ]]
1559+ chosen = min (number_of_ways , key = lambda p : (number_of_ways [p ], str (p )))
1560+ act1 = next (act for precond , act in sorted (self .agenda , key = str ) if precond == chosen )
1561+ return chosen , act1 , actions_for_precondition [chosen ]
15631562
15641563 def find_action_for_precondition (self , oprec ):
15651564 """Find action for a given precondition"""
@@ -1739,61 +1738,131 @@ def display_plan(self):
17391738 print (list (reversed (list (self .toposort (self .convert (self .constraints ))))))
17401739
17411740 def execute (self , display = True ):
1742- """Execute the algorithm"""
1743-
1744- step = 1
1745- while len (self .agenda ) > 0 :
1746- step += 1
1747- # select <G, act1> from Agenda
1748- try :
1749- G , act1 , possible_actions = self .find_open_precondition ()
1750- except IndexError :
1751- print ('Probably Wrong' )
1752- break
1741+ """
1742+ Execute the algorithm with backtracking, using iterative deepening on the
1743+ number of actions in the plan. The original greedy version committed to
1744+ the first achiever it happened to iterate over and could not recover when
1745+ that action's own preconditions turned out to be unsatisfiable, so it
1746+ depended on hash ordering and often printed 'Probably Wrong' / "Couldn't
1747+ find a solution". Backtracking over both action choices and threat
1748+ resolution (promotion vs demotion), together with the deterministic
1749+ selection in find_open_precondition and a smallest-plan-first deepening
1750+ bound, makes the planner solve the standard problems reproducibly and
1751+ return a short, valid plan.
1752+ """
1753+ pristine = self ._snapshot ()
1754+ for limit in range (1 , self ._max_plan_actions + 1 ):
1755+ self ._restore (pristine )
1756+ if self ._search ([self ._max_expansions ], limit ):
1757+ if display :
1758+ self .display_plan ()
1759+ else :
1760+ return self .constraints , self .causal_links
1761+ return
1762+ print ("Couldn't find a solution" )
1763+ if not display :
1764+ return None , None
1765+
1766+ def _reachable (self , source , target ):
1767+ """True if target is forced to come after source by the ordering constraints"""
1768+
1769+ stack , seen = [source ], set ()
1770+ while stack :
1771+ node = stack .pop ()
1772+ if node == target :
1773+ return True
1774+ if node in seen :
1775+ continue
1776+ seen .add (node )
1777+ stack .extend (b for a , b in self .constraints if a == node )
1778+ return False
17531779
1754- act0 = possible_actions [0 ]
1755- # remove <G, act1> from Agenda
1756- self .agenda .remove ((G , act1 ))
1780+ def _open_threat (self ):
1781+ """
1782+ Return an (action, causal_link) threat that is not yet resolved by the
1783+ ordering constraints, or None if every causal link is protected. A
1784+ causal link (a0, p, a1) is threatened by an action whose effect negates p
1785+ unless the action is already ordered before a0 (promotion) or after a1
1786+ (demotion).
1787+ """
1788+ for a0 , p , a1 in self .causal_links :
1789+ for action in self .actions :
1790+ if action == a0 or action == a1 :
1791+ continue
1792+ if any (self .is_a_threat (p , effect ) for effect in action .effect ):
1793+ if not (self ._reachable (action , a0 ) or self ._reachable (a1 , action )):
1794+ return action , (a0 , p , a1 )
1795+ return None
17571796
1758- # For actions with variable number of arguments, use least commitment principle
1759- # act0_temp, bindings = self.find_action_for_precondition(G)
1760- # act0 = self.generate_action_object(act0_temp, bindings)
1797+ def _snapshot (self ):
1798+ return set (self .actions ), set (self .constraints ), list (self .causal_links ), set (self .agenda )
17611799
1762- # Actions = Actions U {act0}
1763- self .actions .add (act0 )
1800+ def _restore (self , snapshot ):
1801+ self .actions , self .constraints , self .causal_links , self .agenda = (
1802+ set (snapshot [0 ]), set (snapshot [1 ]), list (snapshot [2 ]), set (snapshot [3 ]))
17641803
1765- # Constraints = add_const(start < act0, Constraints)
1766- self .constraints = self .add_const ((self .start , act0 ), self .constraints )
1804+ def _search (self , budget , limit ):
1805+ """
1806+ Recursively complete the partial plan, backtracking on failure. Three
1807+ kinds of choice points are explored: which action satisfies an open
1808+ precondition, how each threat is resolved (promotion vs demotion), and -
1809+ bounded by 'limit' - whether to introduce a new action at all. Returns
1810+ True and leaves the solution in self.* on success.
1811+ """
1812+ if budget [0 ] <= 0 :
1813+ return False
1814+ budget [0 ] -= 1
1815+
1816+ # first, resolve any outstanding threat to a causal link (choice point)
1817+ threat = self ._open_threat ()
1818+ if threat is not None :
1819+ action , (a0 , p , a1 ) = threat
1820+ snapshot = self ._snapshot ()
1821+ for ordering in ((action , a0 ), (a1 , action )): # promotion, then demotion
1822+ new_constraints = self .add_const (ordering , self .constraints )
1823+ if ordering in new_constraints : # ordering was consistent (acyclic and allowed)
1824+ self .constraints = new_constraints
1825+ if self ._search (budget , limit ):
1826+ return True
1827+ self ._restore (snapshot )
1828+ return False
17671829
1768- # for each CL E CausalLinks do
1769- # Constraints = protect(CL, act0, Constraints)
1770- for causal_link in self .causal_links :
1771- self .constraints = self .protect (causal_link , act0 , self .constraints )
1830+ # no open threats: a plan with an empty agenda is a complete solution
1831+ if not self .agenda :
1832+ return True
17721833
1773- # Agenda = Agenda U {<P, act0>: P is a precondition of act0}
1774- for precondition in act0 .precond :
1775- self .agenda .add ((precondition , act0 ))
1834+ # select <G, act1> from the agenda (most-constrained precondition first)
1835+ G , act1 , possible_actions = self .find_open_precondition ()
1836+ if G is None : # an open precondition has no achiever -> dead end
1837+ return False
17761838
1777- # Constraints = add_const(act0 < act1, Constraints)
1839+ # number of actions already introduced, excluding the dummy Start/Finish
1840+ introduced = len (self .actions ) - 2
1841+ snapshot = self ._snapshot ()
1842+ # try each achiever deterministically, reusing existing actions first
1843+ for act0 in sorted (set (possible_actions ), key = lambda a : (a not in self .actions , str (a ))):
1844+ is_new = act0 not in self .actions
1845+ if is_new and introduced >= limit : # deepening bound on plan size
1846+ continue
1847+ self .agenda .discard ((G , act1 ))
1848+ self .actions .add (act0 )
1849+ self .constraints = self .add_const ((self .start , act0 ), self .constraints )
17781850 self .constraints = self .add_const ((act0 , act1 ), self .constraints )
1779-
1780- # CausalLinks U {<act0, G, act1>}
1781- if (act0 , G , act1 ) not in self .causal_links :
1782- self .causal_links .append ((act0 , G , act1 ))
1783-
1784- # for each A E Actions do
1785- # Constraints = protect(<act0, G, act1>, A, Constraints)
1786- for action in self .actions :
1787- self .constraints = self .protect ((act0 , G , act1 ), action , self .constraints )
1788-
1789- if step > 200 :
1790- print ("Couldn't find a solution" )
1791- return None , None
1792-
1793- if display :
1794- self .display_plan ()
1795- else :
1796- return self .constraints , self .causal_links
1851+ # the causal link act0 --G--> act1 requires act0 strictly before act1
1852+ # (and after start); add_const drops an ordering that would create a
1853+ # cycle, so reject the choice when the required ordering is not enforced
1854+ if ((act0 == act1 or self ._reachable (act0 , act1 )) and
1855+ (act0 == self .start or self ._reachable (self .start , act0 ))):
1856+ if (act0 , G , act1 ) not in self .causal_links :
1857+ self .causal_links .append ((act0 , G , act1 ))
1858+ if is_new : # a freshly introduced action contributes its own preconditions
1859+ for precondition in act0 .precond :
1860+ self .agenda .add ((precondition , act0 ))
1861+ if self ._search (budget , limit ):
1862+ return True
1863+ # undo and try the next achiever
1864+ self ._restore (snapshot )
1865+ return False
17971866
17981867
17991868def spare_tire_graph_plan ():
0 commit comments