diff --git a/planning.py b/planning.py index 52e4c0b36..9e52c839e 100644 --- a/planning.py +++ b/planning.py @@ -1,13 +1,134 @@ """Planning (Chapters 10-11) """ -# flake8: noqa +from utils import Expr, expr, first +from logic import FolKB -import agents +class PDLL: + """ + PDLL used to deine a search problem + It stores states in a knowledge base consisting of first order logic statements + The conjunction of these logical statements completely define a state + """ -import math -import random -import sys -import time -import bisect -import string + def __init__(self, initial_state, actions, goal_test): + self.kb = FolKB(initial_state) + self.actions = actions + self.goal_test_func = goal_test + + def goal_test(self): + return self.goal_test_func(self.kb) + + def act(self, action): + """ + Performs the action given as argument + Note that action is an Expr like expr('Remove(Glass, Table)') or expr('Eat(Sandwich)') + """ + action_name = action.op + 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)) + if not list_action.check_precond(self.kb, args): + raise Exception("Action '{}' pre-conditions not satisfied".format(action)) + list_action(self.kb, args) + +class Action: + """ + Defines an action schema using preconditions and effects + Use this to describe actions in PDDL + action is an Expr where variables are given as arguments(args) + Precondition and effect are both lists with positive and negated literals + Example: + precond_pos = [expr("Human(person)"), expr("Hungry(Person)")] + precond_neg = [expr("Eaten(food)")] + effect_add = [expr("Eaten(food)")] + effect_rem = [expr("Hungry(person)")] + eat = Action(expr("Eat(person, food)"), [precond_pos, precond_neg], [effect_add, effect_rem]) + """ + + def __init__(self,action , precond, effect): + self.name = action.op + self.args = action.args + self.precond_pos = precond[0] + self.precond_neg = precond[1] + self.effect_add = effect[0] + self.effect_rem = effect[1] + + def __call__(self, kb, args): + return self.act(kb, args) + + def substitute(self, e, args): + """Replaces variables in expression with their respective Propostional symbol""" + new_args = [args[i] for x in e.args for i in range(len(self.args)) if self.args[i]==x] + return Expr(e.op, *new_args) + + def check_precond(self, kb, args): + """Checks if the precondition is satisfied in the current state""" + #check for positive clauses + for clause in self.precond_pos: + if self.substitute(clause, args) not in kb.clauses: + return False + #check for negative clauses + for clause in self.precond_neg: + if self.substitute(clause, args) in kb.clauses: + return False + return True + + def act(self, kb, args): + """Executes the action on the state's kb""" + #check if the preconditions are satisfied + if not self.check_precond(kb, args): + raise Exception("Action pre-conditions not satisfied") + #remove negative literals + for clause in self.effect_rem: + kb.retract(self.substitute(clause, args)) + #add positive literals + for clause in self.effect_add: + kb.tell(self.substitute(clause, args)) + + +def air_cargo(): + init = [expr('At(C1, SFO)'), + expr('At(C2, JFK)'), + expr('At(P1, SFO)'), + expr('At(P2, JFK)'), + expr('Cargo(C1)'), + expr('Cargo(C2)'), + expr('Plane(P1)'), + expr('Plane(P2)'), + expr('Airport(JFK)'), + expr('Airport(SFO)')] + + def goal_test(kb): + required = [expr('At(C1 , JFK)'), expr('At(C2 ,SFO)')] + for q in required: + if kb.ask(q) is False: + return False + return True + + ## Actions + # Load + precond_pos = [expr("At(c, a)"), expr("At(p, a)"), expr("Cargo(c)"), expr("Plane(p)"), expr("Airport(a)")] + precond_neg = [] + effect_add = [expr("In(c, p)")] + effect_rem = [expr("At(c, a)")] + load = Action(expr("Load(c, p, a)"), [precond_pos, precond_neg], [effect_add, effect_rem]) + + # Unload + precond_pos = [expr("In(c, p)"), expr("At(p, a)"), expr("Cargo(c)"), expr("Plane(p)"), expr("Airport(a)")] + precond_neg = [] + effect_add = [expr("At(c, a)")] + effect_rem = [expr("In(c, p)")] + unload = Action(expr("Unload(c, p, a)"), [precond_pos, precond_neg], [effect_add, effect_rem]) + + # Load + # Used used 'f' instead of 'from' because 'from' is a python keyword and expr uses eval() function + precond_pos = [expr("At(p, f)"), expr("Plane(p)"), expr("Airport(f)"), expr("Airport(to)")] + precond_neg = [] + effect_add = [expr("At(p, to)")] + effect_rem = [expr("At(p, f)")] + fly = Action(expr("Fly(p, f, to)"), [precond_pos, precond_neg], [effect_add, effect_rem]) + + return PDLL(init, [load, unload, fly], goal_test) + diff --git a/tests/test_planning.py b/tests/test_planning.py new file mode 100644 index 000000000..aed4812ea --- /dev/null +++ b/tests/test_planning.py @@ -0,0 +1,34 @@ +from planning import * +from utils import expr +from logic import FolKB + +def test_action(): + precond = [[expr("P(x)"), expr("Q(y, z)")] + ,[expr("Q(x)")]] + effect = [[expr("Q(x)")] + , [expr("P(x)")]] + a=Action(expr("A(x,y,z)"),precond, effect) + args = [expr("A"), expr("B"), expr("C")] + assert a.substitute(expr("P(x, z, y)"), args) == expr("P(A, C, B)") + test_kb = FolKB([expr("P(A)"), expr("Q(B, C)"), expr("R(D)")]) + assert a.check_precond(test_kb, args) + a.act(test_kb, args) + assert test_kb.ask(expr("P(A)")) is False + assert test_kb.ask(expr("Q(A)")) is not False + assert test_kb.ask(expr("Q(B, C)")) is not False + assert not a.check_precond(test_kb, args) + +def test_air_cargo(): + p = air_cargo() + assert p.goal_test() is False + solution =[expr("Load(C1 , P1, SFO)"), + expr("Fly(P1, SFO, JFK)"), + expr("Unload(C1, P1, JFK)"), + expr("Load(C2, P2, JFK)"), + expr("Fly(P2, JFK, SFO)"), + expr("Unload (C2, P2, SFO)")] + + for action in solution: + p.act(action) + + assert p.goal_test()