Skip to content

Commit b009d1f

Browse files
kaivalyarnorvig
authored andcommitted
Planning implementations - 11.1 and 11.5 (aimacode#505)
* define HLA, Problem and implement 11.1 * add demonstration of job_shop_problem * implementing 11.5 * adding test for refinement
1 parent 5ea1fb6 commit b009d1f

File tree

2 files changed

+359
-1
lines changed

2 files changed

+359
-1
lines changed

planning.py

Lines changed: 311 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"""
33

44
import itertools
5-
from utils import Expr, expr, first
5+
from search import Node
6+
from utils import Expr, expr, first, FIFOQueue
67
from logic import FolKB
78

89

@@ -574,3 +575,312 @@ def goal_test(kb):
574575
go = Action(expr("Go(actor, to)"), [precond_pos, precond_neg], [effect_add, effect_rem])
575576

576577
return PDDL(init, [hit, go], goal_test)
578+
579+
580+
class HLA(Action):
581+
"""
582+
Define Actions for the real-world (that may be refined further), and satisfy resource
583+
constraints.
584+
"""
585+
unique_group = 1
586+
587+
def __init__(self, action, precond=[None, None], effect=[None, None], duration=0,
588+
consume={}, use={}):
589+
"""
590+
As opposed to actions, to define HLA, we have added constraints.
591+
duration holds the amount of time required to execute the task
592+
consumes holds a dictionary representing the resources the task consumes
593+
uses holds a dictionary representing the resources the task uses
594+
"""
595+
super().__init__(action, precond, effect)
596+
self.duration = duration
597+
self.consumes = consume
598+
self.uses = use
599+
self.completed = False
600+
# self.priority = -1 # must be assigned in relation to other HLAs
601+
# self.job_group = -1 # must be assigned in relation to other HLAs
602+
603+
def do_action(self, job_order, available_resources, kb, args):
604+
"""
605+
An HLA based version of act - along with knowledge base updation, it handles
606+
resource checks, and ensures the actions are executed in the correct order.
607+
"""
608+
# print(self.name)
609+
if not self.has_usable_resource(available_resources):
610+
raise Exception('Not enough usable resources to execute {}'.format(self.name))
611+
if not self.has_consumable_resource(available_resources):
612+
raise Exception('Not enough consumable resources to execute {}'.format(self.name))
613+
if not self.inorder(job_order):
614+
raise Exception("Can't execute {} - execute prerequisite actions first".
615+
format(self.name))
616+
super().act(kb, args) # update knowledge base
617+
for resource in self.consumes: # remove consumed resources
618+
available_resources[resource] -= self.consumes[resource]
619+
self.completed = True # set the task status to complete
620+
621+
def has_consumable_resource(self, available_resources):
622+
"""
623+
Ensure there are enough consumable resources for this action to execute.
624+
"""
625+
for resource in self.consumes:
626+
if available_resources.get(resource) is None:
627+
return False
628+
if available_resources[resource] < self.consumes[resource]:
629+
return False
630+
return True
631+
632+
def has_usable_resource(self, available_resources):
633+
"""
634+
Ensure there are enough usable resources for this action to execute.
635+
"""
636+
for resource in self.uses:
637+
if available_resources.get(resource) is None:
638+
return False
639+
if available_resources[resource] < self.uses[resource]:
640+
return False
641+
return True
642+
643+
def inorder(self, job_order):
644+
"""
645+
Ensure that all the jobs that had to be executed before the current one have been
646+
successfully executed.
647+
"""
648+
for jobs in job_order:
649+
if self in jobs:
650+
for job in jobs:
651+
if job is self:
652+
return True
653+
if not job.completed:
654+
return False
655+
return True
656+
657+
658+
class Problem(PDDL):
659+
"""
660+
Define real-world problems by aggregating resources as numerical quantities instead of
661+
named entities.
662+
663+
This class is identical to PDLL, except that it overloads the act function to handle
664+
resource and ordering conditions imposed by HLA as opposed to Action.
665+
"""
666+
def __init__(self, initial_state, actions, goal_test, jobs=None, resources={}):
667+
super().__init__(initial_state, actions, goal_test)
668+
self.jobs = jobs
669+
self.resources = resources
670+
671+
def act(self, action):
672+
"""
673+
Performs the HLA given as argument.
674+
675+
Note that this is different from the superclass action - where the parameter was an
676+
Expression. For real world problems, an Expr object isn't enough to capture all the
677+
detail required for executing the action - resources, preconditions, etc need to be
678+
checked for too.
679+
"""
680+
args = action.args
681+
list_action = first(a for a in self.actions if a.name == action.name)
682+
if list_action is None:
683+
raise Exception("Action '{}' not found".format(action.name))
684+
list_action.do_action(self.jobs, self.resources, self.kb, args)
685+
# print(self.resources)
686+
687+
def refinements(hla, state, library): # TODO - refinements may be (multiple) HLA themselves ...
688+
"""
689+
state is a Problem, containing the current state kb
690+
library is a dictionary containing details for every possible refinement. eg:
691+
{
692+
"HLA": [
693+
"Go(Home,SFO)",
694+
"Go(Home,SFO)",
695+
"Drive(Home, SFOLongTermParking)",
696+
"Shuttle(SFOLongTermParking, SFO)",
697+
"Taxi(Home, SFO)"
698+
],
699+
"steps": [
700+
["Drive(Home, SFOLongTermParking)", "Shuttle(SFOLongTermParking, SFO)"],
701+
["Taxi(Home, SFO)"],
702+
[], # empty refinements ie primitive action
703+
[],
704+
[]
705+
],
706+
"precond_pos": [
707+
["At(Home), Have(Car)"],
708+
["At(Home)"],
709+
["At(Home)", "Have(Car)"]
710+
["At(SFOLongTermParking)"]
711+
["At(Home)"]
712+
],
713+
"precond_neg": [[],[],[],[],[]],
714+
"effect_pos": [
715+
["At(SFO)"],
716+
["At(SFO)"],
717+
["At(SFOLongTermParking)"],
718+
["At(SFO)"],
719+
["At(SFO)"]
720+
],
721+
"effect_neg": [
722+
["At(Home)"],
723+
["At(Home)"],
724+
["At(Home)"],
725+
["At(SFOLongTermParking)"],
726+
["At(Home)"]
727+
]
728+
}
729+
"""
730+
e = Expr(hla.name, hla.args)
731+
indices = [i for i,x in enumerate(library["HLA"]) if expr(x).op == hla.name]
732+
for i in indices:
733+
action = HLA(expr(library["steps"][i][0]), [ # TODO multiple refinements
734+
[expr(x) for x in library["precond_pos"][i]],
735+
[expr(x) for x in library["precond_neg"][i]]
736+
],
737+
[
738+
[expr(x) for x in library["effect_pos"][i]],
739+
[expr(x) for x in library["effect_neg"][i]]
740+
])
741+
if action.check_precond(state.kb, action.args):
742+
yield action
743+
744+
def hierarchical_search(problem, hierarchy):
745+
"""
746+
[Figure 11.5] 'Hierarchical Search, a Breadth First Search implementation of Hierarchical
747+
Forward Planning Search'
748+
749+
The problem is a real-world prodlem defined by the problem class, and the hierarchy is
750+
a dictionary of HLA - refinements (see refinements generator for details)
751+
"""
752+
act = Node(problem.actions[0])
753+
frontier = FIFOQueue()
754+
frontier.append(act)
755+
while(True):
756+
if not frontier: #(len(frontier)==0):
757+
return None
758+
plan = frontier.pop()
759+
print(plan.state.name)
760+
hla = plan.state #first_or_null(plan)
761+
prefix = None
762+
if plan.parent:
763+
prefix = plan.parent.state.action #prefix, suffix = subseq(plan.state, hla)
764+
outcome = Problem.result(problem, prefix)
765+
if hla is None:
766+
if outcome.goal_test():
767+
return plan.path()
768+
else:
769+
print("else")
770+
for sequence in Problem.refinements(hla, outcome, hierarchy):
771+
print("...")
772+
frontier.append(Node(plan.state, plan.parent, sequence))
773+
774+
def result(problem, action):
775+
"""The outcome of applying an action to the current problem"""
776+
if action is not None:
777+
problem.act(action)
778+
return problem
779+
else:
780+
return problem
781+
782+
783+
def job_shop_problem():
784+
"""
785+
[figure 11.1] JOB-SHOP-PROBLEM
786+
787+
A job-shop scheduling problem for assembling two cars,
788+
with resource and ordering constraints.
789+
790+
Example:
791+
>>> from planning import *
792+
>>> p = job_shop_problem()
793+
>>> p.goal_test()
794+
False
795+
>>> p.act(p.jobs[1][0])
796+
>>> p.act(p.jobs[1][1])
797+
>>> p.act(p.jobs[1][2])
798+
>>> p.act(p.jobs[0][0])
799+
>>> p.act(p.jobs[0][1])
800+
>>> p.goal_test()
801+
False
802+
>>> p.act(p.jobs[0][2])
803+
>>> p.goal_test()
804+
True
805+
>>>
806+
"""
807+
init = [expr('Car(C1)'),
808+
expr('Car(C2)'),
809+
expr('Wheels(W1)'),
810+
expr('Wheels(W2)'),
811+
expr('Engine(E2)'),
812+
expr('Engine(E2)')]
813+
814+
def goal_test(kb):
815+
# print(kb.clauses)
816+
required = [expr('Has(C1, W1)'), expr('Has(C1, E1)'), expr('Inspected(C1)'),
817+
expr('Has(C2, W2)'), expr('Has(C2, E2)'), expr('Inspected(C2)')]
818+
for q in required:
819+
# print(q)
820+
# print(kb.ask(q))
821+
if kb.ask(q) is False:
822+
return False
823+
return True
824+
825+
resources = {'EngineHoists': 1, 'WheelStations': 2, 'Inspectors': 2, 'LugNuts': 500}
826+
827+
# AddEngine1
828+
precond_pos = []
829+
precond_neg = [expr("Has(C1,E1)")]
830+
effect_add = [expr("Has(C1,E1)")]
831+
effect_rem = []
832+
add_engine1 = HLA(expr("AddEngine1"),
833+
[precond_pos, precond_neg], [effect_add, effect_rem],
834+
duration=30, use={'EngineHoists': 1})
835+
836+
# AddEngine2
837+
precond_pos = []
838+
precond_neg = [expr("Has(C2,E2)")]
839+
effect_add = [expr("Has(C2,E2)")]
840+
effect_rem = []
841+
add_engine2 = HLA(expr("AddEngine2"),
842+
[precond_pos, precond_neg], [effect_add, effect_rem],
843+
duration=60, use={'EngineHoists': 1})
844+
845+
# AddWheels1
846+
precond_pos = []
847+
precond_neg = [expr("Has(C1,W1)")]
848+
effect_add = [expr("Has(C1,W1)")]
849+
effect_rem = []
850+
add_wheels1 = HLA(expr("AddWheels1"),
851+
[precond_pos, precond_neg], [effect_add, effect_rem],
852+
duration=30, consume={'LugNuts': 20}, use={'WheelStations': 1})
853+
854+
# AddWheels2
855+
precond_pos = []
856+
precond_neg = [expr("Has(C2,W2)")]
857+
effect_add = [expr("Has(C2,W2)")]
858+
effect_rem = []
859+
add_wheels2 = HLA(expr("AddWheels2"),
860+
[precond_pos, precond_neg], [effect_add, effect_rem],
861+
duration=15, consume={'LugNuts': 20}, use={'WheelStations': 1})
862+
863+
# Inspect1
864+
precond_pos = []
865+
precond_neg = [expr("Inspected(C1)")]
866+
effect_add = [expr("Inspected(C1)")]
867+
effect_rem = []
868+
inspect1 = HLA(expr("Inspect1"),
869+
[precond_pos, precond_neg], [effect_add, effect_rem],
870+
duration=10, use={'Inspectors': 1})
871+
872+
# Inspect2
873+
precond_pos = []
874+
precond_neg = [expr("Inspected(C2)")]
875+
effect_add = [expr("Inspected(C2)")]
876+
effect_rem = []
877+
inspect2 = HLA(expr("Inspect2"),
878+
[precond_pos, precond_neg], [effect_add, effect_rem],
879+
duration=10, use={'Inspectors': 1})
880+
881+
job_group1 = [add_engine1, add_wheels1, inspect1]
882+
job_group2 = [add_engine2, add_wheels2, inspect2]
883+
884+
return Problem(init, [add_engine1, add_engine2, add_wheels1, add_wheels2, inspect1, inspect2],
885+
goal_test, [job_group1, job_group2], resources)
886+

tests/test_planning.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,51 @@ def test_graph_call():
8181
graph()
8282

8383
assert levels_size == len(graph.levels) - 1
84+
85+
86+
def test_job_shop_problem():
87+
p = job_shop_problem()
88+
assert p.goal_test() is False
89+
90+
solution = [p.jobs[1][0],
91+
p.jobs[0][0],
92+
p.jobs[0][1],
93+
p.jobs[0][2],
94+
p.jobs[1][1],
95+
p.jobs[1][2]]
96+
97+
for action in solution:
98+
p.act(action)
99+
100+
assert p.goal_test()
101+
102+
def test_refinements() :
103+
init = [expr('At(Home)')]
104+
def goal_test(kb):
105+
return kb.ask(expr('At(SFO)'))
106+
107+
library = {"HLA": ["Go(Home,SFO)","Taxi(Home, SFO)"],
108+
"steps": [["Taxi(Home, SFO)"],[]],
109+
"precond_pos": [["At(Home)"],["At(Home)"]],
110+
"precond_neg": [[],[]],
111+
"effect_pos": [["At(SFO)"],["At(SFO)"]],
112+
"effect_neg": [["At(Home)"],["At(Home)"],]}
113+
# Go SFO
114+
precond_pos = [expr("At(Home)")]
115+
precond_neg = []
116+
effect_add = [expr("At(SFO)")]
117+
effect_rem = [expr("At(Home)")]
118+
go_SFO = HLA(expr("Go(Home,SFO)"),
119+
[precond_pos, precond_neg], [effect_add, effect_rem])
120+
# Taxi SFO
121+
precond_pos = [expr("At(Home)")]
122+
precond_neg = []
123+
effect_add = [expr("At(SFO)")]
124+
effect_rem = [expr("At(Home)")]
125+
taxi_SFO = HLA(expr("Go(Home,SFO)"),
126+
[precond_pos, precond_neg], [effect_add, effect_rem])
127+
prob = Problem(init, [go_SFO, taxi_SFO], goal_test)
128+
result = [i for i in Problem.refinements(go_SFO, prob, library)]
129+
assert(len(result) == 1)
130+
assert(result[0].name == "Taxi")
131+
assert(result[0].args == (expr("Home"), expr("SFO")))

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy