diff --git a/planning.py b/planning.py index 30b8a79f6..edfb39f19 100644 --- a/planning.py +++ b/planning.py @@ -2,7 +2,8 @@ """ import itertools -from utils import Expr, expr, first +from search import Node +from utils import Expr, expr, first, FIFOQueue from logic import FolKB @@ -574,3 +575,312 @@ def goal_test(kb): go = Action(expr("Go(actor, to)"), [precond_pos, precond_neg], [effect_add, effect_rem]) return PDDL(init, [hit, go], goal_test) + + +class HLA(Action): + """ + Define Actions for the real-world (that may be refined further), and satisfy resource + constraints. + """ + unique_group = 1 + + def __init__(self, action, precond=[None, None], effect=[None, None], duration=0, + consume={}, use={}): + """ + As opposed to actions, to define HLA, we have added constraints. + duration holds the amount of time required to execute the task + consumes holds a dictionary representing the resources the task consumes + uses holds a dictionary representing the resources the task uses + """ + super().__init__(action, precond, effect) + self.duration = duration + self.consumes = consume + self.uses = use + self.completed = False + # self.priority = -1 # must be assigned in relation to other HLAs + # self.job_group = -1 # must be assigned in relation to other HLAs + + def do_action(self, job_order, available_resources, kb, args): + """ + An HLA based version of act - along with knowledge base updation, it handles + resource checks, and ensures the actions are executed in the correct order. + """ + # print(self.name) + if not self.has_usable_resource(available_resources): + raise Exception('Not enough usable resources to execute {}'.format(self.name)) + if not self.has_consumable_resource(available_resources): + raise Exception('Not enough consumable resources to execute {}'.format(self.name)) + if not self.inorder(job_order): + raise Exception("Can't execute {} - execute prerequisite actions first". + format(self.name)) + super().act(kb, args) # update knowledge base + for resource in self.consumes: # remove consumed resources + available_resources[resource] -= self.consumes[resource] + self.completed = True # set the task status to complete + + def has_consumable_resource(self, available_resources): + """ + Ensure there are enough consumable resources for this action to execute. + """ + for resource in self.consumes: + if available_resources.get(resource) is None: + return False + if available_resources[resource] < self.consumes[resource]: + return False + return True + + def has_usable_resource(self, available_resources): + """ + Ensure there are enough usable resources for this action to execute. + """ + for resource in self.uses: + if available_resources.get(resource) is None: + return False + if available_resources[resource] < self.uses[resource]: + return False + return True + + def inorder(self, job_order): + """ + Ensure that all the jobs that had to be executed before the current one have been + successfully executed. + """ + for jobs in job_order: + if self in jobs: + for job in jobs: + if job is self: + return True + if not job.completed: + return False + return True + + +class Problem(PDDL): + """ + Define real-world problems by aggregating resources as numerical quantities instead of + named entities. + + This class is identical to PDLL, except that it overloads the act function to handle + resource and ordering conditions imposed by HLA as opposed to Action. + """ + def __init__(self, initial_state, actions, goal_test, jobs=None, resources={}): + super().__init__(initial_state, actions, goal_test) + self.jobs = jobs + self.resources = resources + + def act(self, action): + """ + Performs the HLA given as argument. + + Note that this is different from the superclass action - where the parameter was an + Expression. For real world problems, an Expr object isn't enough to capture all the + detail required for executing the action - resources, preconditions, etc need to be + checked for too. + """ + args = action.args + list_action = first(a for a in self.actions if a.name == action.name) + if list_action is None: + raise Exception("Action '{}' not found".format(action.name)) + list_action.do_action(self.jobs, self.resources, self.kb, args) + # print(self.resources) + + def refinements(hla, state, library): # TODO - refinements may be (multiple) HLA themselves ... + """ + state is a Problem, containing the current state kb + library is a dictionary containing details for every possible refinement. eg: + { + "HLA": [ + "Go(Home,SFO)", + "Go(Home,SFO)", + "Drive(Home, SFOLongTermParking)", + "Shuttle(SFOLongTermParking, SFO)", + "Taxi(Home, SFO)" + ], + "steps": [ + ["Drive(Home, SFOLongTermParking)", "Shuttle(SFOLongTermParking, SFO)"], + ["Taxi(Home, SFO)"], + [], # empty refinements ie primitive action + [], + [] + ], + "precond_pos": [ + ["At(Home), Have(Car)"], + ["At(Home)"], + ["At(Home)", "Have(Car)"] + ["At(SFOLongTermParking)"] + ["At(Home)"] + ], + "precond_neg": [[],[],[],[],[]], + "effect_pos": [ + ["At(SFO)"], + ["At(SFO)"], + ["At(SFOLongTermParking)"], + ["At(SFO)"], + ["At(SFO)"] + ], + "effect_neg": [ + ["At(Home)"], + ["At(Home)"], + ["At(Home)"], + ["At(SFOLongTermParking)"], + ["At(Home)"] + ] + } + """ + e = Expr(hla.name, hla.args) + indices = [i for i,x in enumerate(library["HLA"]) if expr(x).op == hla.name] + for i in indices: + action = HLA(expr(library["steps"][i][0]), [ # TODO multiple refinements + [expr(x) for x in library["precond_pos"][i]], + [expr(x) for x in library["precond_neg"][i]] + ], + [ + [expr(x) for x in library["effect_pos"][i]], + [expr(x) for x in library["effect_neg"][i]] + ]) + if action.check_precond(state.kb, action.args): + yield action + + def hierarchical_search(problem, hierarchy): + """ + [Figure 11.5] 'Hierarchical Search, a Breadth First Search implementation of Hierarchical + Forward Planning Search' + + The problem is a real-world prodlem defined by the problem class, and the hierarchy is + a dictionary of HLA - refinements (see refinements generator for details) + """ + act = Node(problem.actions[0]) + frontier = FIFOQueue() + frontier.append(act) + while(True): + if not frontier: #(len(frontier)==0): + return None + plan = frontier.pop() + print(plan.state.name) + hla = plan.state #first_or_null(plan) + prefix = None + if plan.parent: + prefix = plan.parent.state.action #prefix, suffix = subseq(plan.state, hla) + outcome = Problem.result(problem, prefix) + if hla is None: + if outcome.goal_test(): + return plan.path() + else: + print("else") + for sequence in Problem.refinements(hla, outcome, hierarchy): + print("...") + frontier.append(Node(plan.state, plan.parent, sequence)) + + def result(problem, action): + """The outcome of applying an action to the current problem""" + if action is not None: + problem.act(action) + return problem + else: + return problem + + +def job_shop_problem(): + """ + [figure 11.1] JOB-SHOP-PROBLEM + + A job-shop scheduling problem for assembling two cars, + with resource and ordering constraints. + + Example: + >>> from planning import * + >>> p = job_shop_problem() + >>> p.goal_test() + False + >>> p.act(p.jobs[1][0]) + >>> p.act(p.jobs[1][1]) + >>> p.act(p.jobs[1][2]) + >>> p.act(p.jobs[0][0]) + >>> p.act(p.jobs[0][1]) + >>> p.goal_test() + False + >>> p.act(p.jobs[0][2]) + >>> p.goal_test() + True + >>> + """ + init = [expr('Car(C1)'), + expr('Car(C2)'), + expr('Wheels(W1)'), + expr('Wheels(W2)'), + expr('Engine(E2)'), + expr('Engine(E2)')] + + def goal_test(kb): + # print(kb.clauses) + required = [expr('Has(C1, W1)'), expr('Has(C1, E1)'), expr('Inspected(C1)'), + expr('Has(C2, W2)'), expr('Has(C2, E2)'), expr('Inspected(C2)')] + for q in required: + # print(q) + # print(kb.ask(q)) + if kb.ask(q) is False: + return False + return True + + resources = {'EngineHoists': 1, 'WheelStations': 2, 'Inspectors': 2, 'LugNuts': 500} + + # AddEngine1 + precond_pos = [] + precond_neg = [expr("Has(C1,E1)")] + effect_add = [expr("Has(C1,E1)")] + effect_rem = [] + add_engine1 = HLA(expr("AddEngine1"), + [precond_pos, precond_neg], [effect_add, effect_rem], + duration=30, use={'EngineHoists': 1}) + + # AddEngine2 + precond_pos = [] + precond_neg = [expr("Has(C2,E2)")] + effect_add = [expr("Has(C2,E2)")] + effect_rem = [] + add_engine2 = HLA(expr("AddEngine2"), + [precond_pos, precond_neg], [effect_add, effect_rem], + duration=60, use={'EngineHoists': 1}) + + # AddWheels1 + precond_pos = [] + precond_neg = [expr("Has(C1,W1)")] + effect_add = [expr("Has(C1,W1)")] + effect_rem = [] + add_wheels1 = HLA(expr("AddWheels1"), + [precond_pos, precond_neg], [effect_add, effect_rem], + duration=30, consume={'LugNuts': 20}, use={'WheelStations': 1}) + + # AddWheels2 + precond_pos = [] + precond_neg = [expr("Has(C2,W2)")] + effect_add = [expr("Has(C2,W2)")] + effect_rem = [] + add_wheels2 = HLA(expr("AddWheels2"), + [precond_pos, precond_neg], [effect_add, effect_rem], + duration=15, consume={'LugNuts': 20}, use={'WheelStations': 1}) + + # Inspect1 + precond_pos = [] + precond_neg = [expr("Inspected(C1)")] + effect_add = [expr("Inspected(C1)")] + effect_rem = [] + inspect1 = HLA(expr("Inspect1"), + [precond_pos, precond_neg], [effect_add, effect_rem], + duration=10, use={'Inspectors': 1}) + + # Inspect2 + precond_pos = [] + precond_neg = [expr("Inspected(C2)")] + effect_add = [expr("Inspected(C2)")] + effect_rem = [] + inspect2 = HLA(expr("Inspect2"), + [precond_pos, precond_neg], [effect_add, effect_rem], + duration=10, use={'Inspectors': 1}) + + job_group1 = [add_engine1, add_wheels1, inspect1] + job_group2 = [add_engine2, add_wheels2, inspect2] + + return Problem(init, [add_engine1, add_engine2, add_wheels1, add_wheels2, inspect1, inspect2], + goal_test, [job_group1, job_group2], resources) + diff --git a/tests/test_planning.py b/tests/test_planning.py index e13bcfd92..0e57ffca6 100644 --- a/tests/test_planning.py +++ b/tests/test_planning.py @@ -81,3 +81,51 @@ def test_graph_call(): graph() assert levels_size == len(graph.levels) - 1 + + +def test_job_shop_problem(): + p = job_shop_problem() + assert p.goal_test() is False + + solution = [p.jobs[1][0], + p.jobs[0][0], + p.jobs[0][1], + p.jobs[0][2], + p.jobs[1][1], + p.jobs[1][2]] + + for action in solution: + p.act(action) + + assert p.goal_test() + +def test_refinements() : + init = [expr('At(Home)')] + def goal_test(kb): + return kb.ask(expr('At(SFO)')) + + library = {"HLA": ["Go(Home,SFO)","Taxi(Home, SFO)"], + "steps": [["Taxi(Home, SFO)"],[]], + "precond_pos": [["At(Home)"],["At(Home)"]], + "precond_neg": [[],[]], + "effect_pos": [["At(SFO)"],["At(SFO)"]], + "effect_neg": [["At(Home)"],["At(Home)"],]} + # Go SFO + precond_pos = [expr("At(Home)")] + precond_neg = [] + effect_add = [expr("At(SFO)")] + effect_rem = [expr("At(Home)")] + go_SFO = HLA(expr("Go(Home,SFO)"), + [precond_pos, precond_neg], [effect_add, effect_rem]) + # Taxi SFO + precond_pos = [expr("At(Home)")] + precond_neg = [] + effect_add = [expr("At(SFO)")] + effect_rem = [expr("At(Home)")] + taxi_SFO = HLA(expr("Go(Home,SFO)"), + [precond_pos, precond_neg], [effect_add, effect_rem]) + prob = Problem(init, [go_SFO, taxi_SFO], goal_test) + result = [i for i in Problem.refinements(go_SFO, prob, library)] + assert(len(result) == 1) + assert(result[0].name == "Taxi") + assert(result[0].args == (expr("Home"), expr("SFO")))