|
2 | 2 | """
|
3 | 3 |
|
4 | 4 | import itertools
|
5 |
| -from utils import Expr, expr, first |
| 5 | +from search import Node |
| 6 | +from utils import Expr, expr, first, FIFOQueue |
6 | 7 | from logic import FolKB
|
7 | 8 |
|
8 | 9 |
|
@@ -574,3 +575,312 @@ def goal_test(kb):
|
574 | 575 | go = Action(expr("Go(actor, to)"), [precond_pos, precond_neg], [effect_add, effect_rem])
|
575 | 576 |
|
576 | 577 | 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 | + |
0 commit comments