from Agenda
+ self.agenda.remove((G, act1))
+
+ # For actions with variable number of arguments, use least commitment principle
+ # act0_temp, bindings = self.find_action_for_precondition(G)
+ # act0 = self.generate_action_object(act0_temp, bindings)
+
+ # Actions = Actions U {act0}
+ self.actions.add(act0)
+
+ # Constraints = add_const(start < act0, Constraints)
+ self.constraints = self.add_const((self.start, act0), self.constraints)
+
+ # for each CL E CausalLinks do
+ # Constraints = protect(CL, act0, Constraints)
+ for causal_link in self.causal_links:
+ self.constraints = self.protect(causal_link, act0, self.constraints)
+
+ # Agenda = Agenda U {: P is a precondition of act0}
+ for precondition in act0.precond:
+ self.agenda.add((precondition, act0))
+
+ # Constraints = add_const(act0 < act1, Constraints)
+ self.constraints = self.add_const((act0, act1), self.constraints)
+
+ # CausalLinks U {}
+ if (act0, G, act1) not in self.causal_links:
+ self.causal_links.append((act0, G, act1))
+
+ # for each A E Actions do
+ # Constraints = protect(, A, Constraints)
+ for action in self.actions:
+ self.constraints = self.protect((act0, G, act1), action, self.constraints)
+
+ if step > 200:
+ print("Couldn't find a solution")
+ return None, None
+
+ if display:
+ self.display_plan()
+ else:
+ return self.constraints, self.causal_links
+
+
+def spare_tire_graphPlan():
+ """Solves the spare tire problem using GraphPlan"""
+ return GraphPlan(spare_tire()).execute()
+
+
+def three_block_tower_graphPlan():
+ """Solves the Sussman Anomaly problem using GraphPlan"""
+ return GraphPlan(three_block_tower()).execute()
+
+
+def air_cargo_graphPlan():
+ """Solves the air cargo problem using GraphPlan"""
+ return GraphPlan(air_cargo()).execute()
+
+
+def have_cake_and_eat_cake_too_graphPlan():
+ """Solves the cake problem using GraphPlan"""
+ return [GraphPlan(have_cake_and_eat_cake_too()).execute()[1]]
+
+
+def shopping_graphPlan():
+ """Solves the shopping problem using GraphPlan"""
+ return GraphPlan(shopping_problem()).execute()
+
+
+def socks_and_shoes_graphPlan():
+ """Solves the socks and shoes problem using GraphPlan"""
+ return GraphPlan(socks_and_shoes()).execute()
+
+
+def simple_blocks_world_graphPlan():
+ """Solves the simple blocks world problem"""
+ return GraphPlan(simple_blocks_world()).execute()
class HLA(Action):
@@ -565,18 +1422,19 @@ class HLA(Action):
"""
unique_group = 1
- def __init__(self, action, precond=[None, None], effect=[None, None], duration=0,
- consume={}, use={}):
+ def __init__(self, action, precond=None, effect=None, duration=0, consume=None, use=None):
"""
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
"""
+ precond = precond or [None]
+ effect = effect or [None]
super().__init__(action, precond, effect)
self.duration = duration
- self.consumes = consume
- self.uses = use
+ self.consumes = consume or {}
+ self.uses = use or {}
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
@@ -586,7 +1444,6 @@ 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):
@@ -594,10 +1451,11 @@ def do_action(self, job_order, available_resources, kb, args):
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
+ kb = 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
+ return kb
def has_consumable_resource(self, available_resources):
"""
@@ -636,18 +1494,19 @@ def inorder(self, job_order):
return True
-class Problem(PDDL):
+class RealWorldPlanningProblem(PlanningProblem):
"""
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
+ This class is identical to PDDL, 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)
+
+ def __init__(self, initial, goals, actions, jobs=None, resources=None):
+ super().__init__(initial, goals, actions)
self.jobs = jobs
- self.resources = resources
+ self.resources = resources or {}
def act(self, action):
"""
@@ -662,106 +1521,249 @@ def act(self, action):
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)
+ self.initial = list_action.do_action(self.jobs, self.resources, self.initial, args).clauses
- def refinements(hla, state, library): # TODO - refinements may be (multiple) HLA themselves ...
+ def refinements(self, library): # 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:
+ State is a Problem, containing the current state kb library is a
+ dictionary containing details for every possible refinement. e.g.:
{
- "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
+ '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)'],
+ [],
[],
[]
- ],
- "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]
+ ],
+ # empty refinements indicate a primitive action
+ 'precond': [
+ ['At(Home) & Have(Car)'],
+ ['At(Home)'],
+ ['At(Home) & Have(Car)'],
+ ['At(SFOLongTermParking)'],
+ ['At(Home)']
+ ],
+ 'effect': [
+ ['At(SFO) & ~At(Home)'],
+ ['At(SFO) & ~At(Home)'],
+ ['At(SFOLongTermParking) & ~At(Home)'],
+ ['At(SFO) & ~At(SFOLongTermParking)'],
+ ['At(SFO) & ~At(Home)']
+ ]}
+ """
+ indices = [i for i, x in enumerate(library['HLA']) if expr(x).op == self.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
+ actions = []
+ for j in range(len(library['steps'][i])):
+ # find the index of the step [j] of the HLA
+ index_step = [k for k, x in enumerate(library['HLA']) if x == library['steps'][i][j]][0]
+ precond = library['precond'][index_step][0] # preconditions of step [j]
+ effect = library['effect'][index_step][0] # effect of step [j]
+ actions.append(HLA(library['steps'][i][j], precond, effect))
+ yield actions
+
+ def hierarchical_search(self, 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
+ The problem is a real-world problem 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()
+ act = Node(self.initial, None, [self.actions[0]])
+ frontier = deque()
frontier.append(act)
- while(True):
+ while True:
if not frontier:
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:
+ plan = frontier.popleft()
+ # finds the first non primitive hla in plan actions
+ (hla, index) = RealWorldPlanningProblem.find_hla(plan, hierarchy)
+ prefix = plan.action[:index]
+ outcome = RealWorldPlanningProblem(
+ RealWorldPlanningProblem.result(self.initial, prefix), self.goals, self.actions)
+ suffix = plan.action[index + 1:]
+ if not hla: # hla is None and plan is primitive
if outcome.goal_test():
- return plan.path()
+ return plan.action
else:
- print("else")
- for sequence in Problem.refinements(hla, outcome, hierarchy):
- print("...")
- frontier.append(Node(plan.state, plan.parent, sequence))
+ for sequence in RealWorldPlanningProblem.refinements(hla, hierarchy): # find refinements
+ frontier.append(Node(outcome.initial, plan, prefix + sequence + suffix))
- def result(problem, action):
+ def result(state, actions):
"""The outcome of applying an action to the current problem"""
- if action is not None:
- problem.act(action)
- return problem
- else:
- return problem
+ for a in actions:
+ if a.check_precond(state, a.args):
+ state = a(state, a.args).clauses
+ return state
+
+ def angelic_search(self, hierarchy, initial_plan):
+ """
+ [Figure 11.8]
+ A hierarchical planning algorithm that uses angelic semantics to identify and
+ commit to high-level plans that work while avoiding high-level plans that don’t.
+ The predicate MAKING-PROGRESS checks to make sure that we aren’t stuck in an infinite regression
+ of refinements.
+ At top level, call ANGELIC-SEARCH with [Act] as the initialPlan.
+
+ InitialPlan contains a sequence of HLA's with angelic semantics
+
+ The possible effects of an angelic HLA in initialPlan are:
+ ~ : effect remove
+ $+: effect possibly add
+ $-: effect possibly remove
+ $$: possibly add or remove
+ """
+ frontier = deque(initial_plan)
+ while True:
+ if not frontier:
+ return None
+ plan = frontier.popleft() # sequence of HLA/Angelic HLA's
+ opt_reachable_set = RealWorldPlanningProblem.reach_opt(self.initial, plan)
+ pes_reachable_set = RealWorldPlanningProblem.reach_pes(self.initial, plan)
+ if self.intersects_goal(opt_reachable_set):
+ if RealWorldPlanningProblem.is_primitive(plan, hierarchy):
+ return [x for x in plan.action]
+ guaranteed = self.intersects_goal(pes_reachable_set)
+ if guaranteed and RealWorldPlanningProblem.making_progress(plan, initial_plan):
+ final_state = guaranteed[0] # any element of guaranteed
+ return RealWorldPlanningProblem.decompose(hierarchy, final_state, pes_reachable_set)
+ # there should be at least one HLA/AngelicHLA, otherwise plan would be primitive
+ hla, index = RealWorldPlanningProblem.find_hla(plan, hierarchy)
+ prefix = plan.action[:index]
+ suffix = plan.action[index + 1:]
+ outcome = RealWorldPlanningProblem(
+ RealWorldPlanningProblem.result(self.initial, prefix), self.goals, self.actions)
+ for sequence in RealWorldPlanningProblem.refinements(hla, hierarchy): # find refinements
+ frontier.append(
+ AngelicNode(outcome.initial, plan, prefix + sequence + suffix, prefix + sequence + suffix))
+
+ def intersects_goal(self, reachable_set):
+ """
+ Find the intersection of the reachable states and the goal
+ """
+ return [y for x in list(reachable_set.keys())
+ for y in reachable_set[x]
+ if all(goal in y for goal in self.goals)]
+
+ def is_primitive(plan, library):
+ """
+ checks if the hla is primitive action
+ """
+ for hla in plan.action:
+ indices = [i for i, x in enumerate(library['HLA']) if expr(x).op == hla.name]
+ for i in indices:
+ if library["steps"][i]:
+ return False
+ return True
+
+ def reach_opt(init, plan):
+ """
+ Finds the optimistic reachable set of the sequence of actions in plan
+ """
+ reachable_set = {0: [init]}
+ optimistic_description = plan.action # list of angelic actions with optimistic description
+ return RealWorldPlanningProblem.find_reachable_set(reachable_set, optimistic_description)
+
+ def reach_pes(init, plan):
+ """
+ Finds the pessimistic reachable set of the sequence of actions in plan
+ """
+ reachable_set = {0: [init]}
+ pessimistic_description = plan.action_pes # list of angelic actions with pessimistic description
+ return RealWorldPlanningProblem.find_reachable_set(reachable_set, pessimistic_description)
+
+ def find_reachable_set(reachable_set, action_description):
+ """
+ Finds the reachable states of the action_description when applied in each state of reachable set.
+ """
+ for i in range(len(action_description)):
+ reachable_set[i + 1] = []
+ if type(action_description[i]) is AngelicHLA:
+ possible_actions = action_description[i].angelic_action()
+ else:
+ possible_actions = action_description
+ for action in possible_actions:
+ for state in reachable_set[i]:
+ if action.check_precond(state, action.args):
+ if action.effect[0]:
+ new_state = action(state, action.args).clauses
+ reachable_set[i + 1].append(new_state)
+ else:
+ reachable_set[i + 1].append(state)
+ return reachable_set
+
+ def find_hla(plan, hierarchy):
+ """
+ Finds the the first HLA action in plan.action, which is not primitive
+ and its corresponding index in plan.action
+ """
+ hla = None
+ index = len(plan.action)
+ for i in range(len(plan.action)): # find the first HLA in plan, that is not primitive
+ if not RealWorldPlanningProblem.is_primitive(Node(plan.state, plan.parent, [plan.action[i]]), hierarchy):
+ hla = plan.action[i]
+ index = i
+ break
+ return hla, index
+
+ def making_progress(plan, initial_plan):
+ """
+ Prevents from infinite regression of refinements
+
+ (infinite regression of refinements happens when the algorithm finds a plan that
+ its pessimistic reachable set intersects the goal inside a call to decompose on
+ the same plan, in the same circumstances)
+ """
+ for i in range(len(initial_plan)):
+ if plan == initial_plan[i]:
+ return False
+ return True
+
+ def decompose(hierarchy, plan, s_f, reachable_set):
+ solution = []
+ i = max(reachable_set.keys())
+ while plan.action_pes:
+ action = plan.action_pes.pop()
+ if i == 0:
+ return solution
+ s_i = RealWorldPlanningProblem.find_previous_state(s_f, reachable_set, i, action)
+ problem = RealWorldPlanningProblem(s_i, s_f, plan.action)
+ angelic_call = RealWorldPlanningProblem.angelic_search(problem, hierarchy,
+ [AngelicNode(s_i, Node(None), [action], [action])])
+ if angelic_call:
+ for x in angelic_call:
+ solution.insert(0, x)
+ else:
+ return None
+ s_f = s_i
+ i -= 1
+ return solution
+
+ def find_previous_state(s_f, reachable_set, i, action):
+ """
+ Given a final state s_f and an action finds a state s_i in reachable_set
+ such that when action is applied to state s_i returns s_f.
+ """
+ s_i = reachable_set[i - 1][0]
+ for state in reachable_set[i - 1]:
+ if s_f in [x for x in RealWorldPlanningProblem.reach_pes(
+ state, AngelicNode(state, None, [action], [action]))[1]]:
+ s_i = state
+ break
+ return s_i
def job_shop_problem():
"""
- [figure 11.1] JOB-SHOP-PROBLEM
+ [Figure 11.1] JOB-SHOP-PROBLEM
A job-shop scheduling problem for assembling two cars,
with resource and ordering constraints.
@@ -783,82 +1785,226 @@ def job_shop_problem():
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})
+ add_engine1 = HLA('AddEngine1', precond='~Has(C1, E1)', effect='Has(C1, E1)', duration=30, use={'EngineHoists': 1})
+ add_engine2 = HLA('AddEngine2', precond='~Has(C2, E2)', effect='Has(C2, E2)', duration=60, use={'EngineHoists': 1})
+ add_wheels1 = HLA('AddWheels1', precond='~Has(C1, W1)', effect='Has(C1, W1)', duration=30, use={'WheelStations': 1},
+ consume={'LugNuts': 20})
+ add_wheels2 = HLA('AddWheels2', precond='~Has(C2, W2)', effect='Has(C2, W2)', duration=15, use={'WheelStations': 1},
+ consume={'LugNuts': 20})
+ inspect1 = HLA('Inspect1', precond='~Inspected(C1)', effect='Inspected(C1)', duration=10, use={'Inspectors': 1})
+ inspect2 = HLA('Inspect2', precond='~Inspected(C2)', effect='Inspected(C2)', duration=10, use={'Inspectors': 1})
+
+ actions = [add_engine1, add_engine2, add_wheels1, add_wheels2, inspect1, inspect2]
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)
+ return RealWorldPlanningProblem(
+ initial='Car(C1) & Car(C2) & Wheels(W1) & Wheels(W2) & Engine(E2) & Engine(E2) & ~Has(C1, E1) & ~Has(C2, '
+ 'E2) & ~Has(C1, W1) & ~Has(C2, W2) & ~Inspected(C1) & ~Inspected(C2)',
+ goals='Has(C1, W1) & Has(C1, E1) & Inspected(C1) & Has(C2, W2) & Has(C2, E2) & Inspected(C2)',
+ actions=actions,
+ jobs=[job_group1, job_group2],
+ resources=resources)
+
+
+def go_to_sfo():
+ """Go to SFO Problem"""
+
+ go_home_sfo1 = HLA('Go(Home, SFO)', precond='At(Home) & Have(Car)', effect='At(SFO) & ~At(Home)')
+ go_home_sfo2 = HLA('Go(Home, SFO)', precond='At(Home)', effect='At(SFO) & ~At(Home)')
+ drive_home_sfoltp = HLA('Drive(Home, SFOLongTermParking)', precond='At(Home) & Have(Car)',
+ effect='At(SFOLongTermParking) & ~At(Home)')
+ shuttle_sfoltp_sfo = HLA('Shuttle(SFOLongTermParking, SFO)', precond='At(SFOLongTermParking)',
+ effect='At(SFO) & ~At(SFOLongTermParking)')
+ taxi_home_sfo = HLA('Taxi(Home, SFO)', precond='At(Home)', effect='At(SFO) & ~At(Home)')
+
+ actions = [go_home_sfo1, go_home_sfo2, drive_home_sfoltp, shuttle_sfoltp_sfo, taxi_home_sfo]
+
+ library = {
+ '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)'],
+ [],
+ [],
+ []
+ ],
+ 'precond': [
+ ['At(Home) & Have(Car)'],
+ ['At(Home)'],
+ ['At(Home) & Have(Car)'],
+ ['At(SFOLongTermParking)'],
+ ['At(Home)']
+ ],
+ 'effect': [
+ ['At(SFO) & ~At(Home)'],
+ ['At(SFO) & ~At(Home)'],
+ ['At(SFOLongTermParking) & ~At(Home)'],
+ ['At(SFO) & ~At(SFOLongTermParking)'],
+ ['At(SFO) & ~At(Home)']]}
+
+ return RealWorldPlanningProblem(initial='At(Home)', goals='At(SFO)', actions=actions), library
+
+
+class AngelicHLA(HLA):
+ """
+ Define Actions for the real-world (that may be refined further), under angelic semantics
+ """
+
+ def __init__(self, action, precond, effect, duration=0, consume=None, use=None):
+ super().__init__(action, precond, effect, duration, consume, use)
+
+ def convert(self, clauses):
+ """
+ Converts strings into Exprs
+ An HLA with angelic semantics can achieve the effects of simple HLA's (add / remove a variable)
+ and furthermore can have following effects on the variables:
+ Possibly add variable ( $+ )
+ Possibly remove variable ( $- )
+ Possibly add or remove a variable ( $$ )
+
+ Overrides HLA.convert function
+ """
+ lib = {'~': 'Not',
+ '$+': 'PosYes',
+ '$-': 'PosNot',
+ '$$': 'PosYesNot'}
+
+ if isinstance(clauses, Expr):
+ clauses = conjuncts(clauses)
+ for i in range(len(clauses)):
+ for ch in lib.keys():
+ if clauses[i].op == ch:
+ clauses[i] = expr(lib[ch] + str(clauses[i].args[0]))
+
+ elif isinstance(clauses, str):
+ for ch in lib.keys():
+ clauses = clauses.replace(ch, lib[ch])
+ if len(clauses) > 0:
+ clauses = expr(clauses)
+
+ try:
+ clauses = conjuncts(clauses)
+ except AttributeError:
+ pass
+
+ return clauses
+
+ def angelic_action(self):
+ """
+ Converts a high level action (HLA) with angelic semantics into all of its corresponding high level actions (HLA).
+ An HLA with angelic semantics can achieve the effects of simple HLA's (add / remove a variable)
+ and furthermore can have following effects for each variable:
+
+ Possibly add variable ( $+: 'PosYes' ) --> corresponds to two HLAs:
+ HLA_1: add variable
+ HLA_2: leave variable unchanged
+
+ Possibly remove variable ( $-: 'PosNot' ) --> corresponds to two HLAs:
+ HLA_1: remove variable
+ HLA_2: leave variable unchanged
+
+ Possibly add / remove a variable ( $$: 'PosYesNot' ) --> corresponds to three HLAs:
+ HLA_1: add variable
+ HLA_2: remove variable
+ HLA_3: leave variable unchanged
+
+
+ example: the angelic action with effects possibly add A and possibly add or remove B corresponds to the
+ following 6 effects of HLAs:
+
+
+ '$+A & $$B': HLA_1: 'A & B' (add A and add B)
+ HLA_2: 'A & ~B' (add A and remove B)
+ HLA_3: 'A' (add A)
+ HLA_4: 'B' (add B)
+ HLA_5: '~B' (remove B)
+ HLA_6: ' ' (no effect)
+
+ """
+
+ effects = [[]]
+ for clause in self.effect:
+ (n, w) = AngelicHLA.compute_parameters(clause)
+ effects = effects * n # create n copies of effects
+ it = range(1)
+ if len(effects) != 0:
+ # split effects into n sublists (separate n copies created in compute_parameters)
+ it = range(len(effects) // n)
+ for i in it:
+ if effects[i]:
+ if clause.args:
+ effects[i] = expr(str(effects[i]) + '&' + str(
+ Expr(clause.op[w:], clause.args[0]))) # make changes in the ith part of effects
+ if n == 3:
+ effects[i + len(effects) // 3] = expr(
+ str(effects[i + len(effects) // 3]) + '&' + str(Expr(clause.op[6:], clause.args[0])))
+ else:
+ effects[i] = expr(
+ str(effects[i]) + '&' + str(expr(clause.op[w:]))) # make changes in the ith part of effects
+ if n == 3:
+ effects[i + len(effects) // 3] = expr(
+ str(effects[i + len(effects) // 3]) + '&' + str(expr(clause.op[6:])))
+
+ else:
+ if clause.args:
+ effects[i] = Expr(clause.op[w:], clause.args[0]) # make changes in the ith part of effects
+ if n == 3:
+ effects[i + len(effects) // 3] = Expr(clause.op[6:], clause.args[0])
+
+ else:
+ effects[i] = expr(clause.op[w:]) # make changes in the ith part of effects
+ if n == 3:
+ effects[i + len(effects) // 3] = expr(clause.op[6:])
+
+ return [HLA(Expr(self.name, self.args), self.precond, effects[i]) for i in range(len(effects))]
+
+ def compute_parameters(clause):
+ """
+ computes n,w
+
+ n = number of HLA effects that the angelic HLA corresponds to
+ w = length of representation of angelic HLA effect
+
+ n = 1, if effect is add
+ n = 1, if effect is remove
+ n = 2, if effect is possibly add
+ n = 2, if effect is possibly remove
+ n = 3, if effect is possibly add or remove
+
+ """
+ if clause.op[:9] == 'PosYesNot':
+ # possibly add/remove variable: three possible effects for the variable
+ n = 3
+ w = 9
+ elif clause.op[:6] == 'PosYes': # possibly add variable: two possible effects for the variable
+ n = 2
+ w = 6
+ elif clause.op[:6] == 'PosNot': # possibly remove variable: two possible effects for the variable
+ n = 2
+ w = 3 # We want to keep 'Not' from 'PosNot' when adding action
+ else: # variable or ~variable
+ n = 1
+ w = 0
+ return n, w
+
+
+class AngelicNode(Node):
+ """
+ Extends the class Node.
+ self.action: contains the optimistic description of an angelic HLA
+ self.action_pes: contains the pessimistic description of an angelic HLA
+ """
+
+ def __init__(self, state, parent=None, action_opt=None, action_pes=None, path_cost=0):
+ super().__init__(state, parent, action_opt, path_cost)
+ self.action_pes = action_pes
diff --git a/planning_angelic_search.ipynb b/planning_angelic_search.ipynb
new file mode 100644
index 000000000..71408e1d9
--- /dev/null
+++ b/planning_angelic_search.ipynb
@@ -0,0 +1,638 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Angelic Search \n",
+ "\n",
+ "Search using angelic semantics (is a hierarchical search), where the agent chooses the implementation of the HLA's.
\n",
+ "The algorithms input is: problem, hierarchy and initialPlan\n",
+ "- problem is of type Problem \n",
+ "- hierarchy is a dictionary consisting of all the actions. \n",
+ "- initialPlan is an approximate description(optimistic and pessimistic) of the agents choices for the implementation.
\n",
+ " initialPlan contains a sequence of HLA's with angelic semantics"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from planning import * \n",
+ "from notebook import psource"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The Angelic search algorithm consists of three parts. \n",
+ "- Search using angelic semantics\n",
+ "- Decompose\n",
+ "- a search in the space of refinements, in a similar way with hierarchical search\n",
+ "\n",
+ "### Searching using angelic semantics\n",
+ "- Find the reachable set (optimistic and pessimistic) of the sequence of angelic HLA in initialPlan\n",
+ " - If the optimistic reachable set doesn't intersect the goal, then there is no solution\n",
+ " - If the pessimistic reachable set intersects the goal, then we call decompose, in order to find the sequence of actions that lead us to the goal. \n",
+ " - If the optimistic reachable set intersects the goal, but the pessimistic doesn't we do some further refinements, in order to see if there is a sequence of actions that achieves the goal. \n",
+ " \n",
+ "### Search in space of refinements\n",
+ "- Create a search tree, that has root the action and children it's refinements\n",
+ "- Extend frontier by adding each refinement, so that we keep looping till we find all primitive actions\n",
+ "- If we achieve that we return the path of the solution (search tree), else there is no solution and we return None.\n",
+ "\n",
+ " \n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " def angelic_search(problem, hierarchy, initialPlan):\n",
+ " """\n",
+ "\t[Figure 11.8] A hierarchical planning algorithm that uses angelic semantics to identify and\n",
+ "\tcommit to high-level plans that work while avoiding high-level plans that don’t. \n",
+ "\tThe predicate MAKING-PROGRESS checks to make sure that we aren’t stuck in an infinite regression\n",
+ "\tof refinements. \n",
+ "\tAt top level, call ANGELIC -SEARCH with [Act ] as the initialPlan .\n",
+ "\n",
+ " initialPlan contains a sequence of HLA's with angelic semantics \n",
+ "\n",
+ " The possible effects of an angelic HLA in initialPlan are : \n",
+ " ~ : effect remove\n",
+ " $+: effect possibly add\n",
+ " $-: effect possibly remove\n",
+ " $$: possibly add or remove\n",
+ "\t"""\n",
+ " frontier = deque(initialPlan)\n",
+ " while True: \n",
+ " if not frontier:\n",
+ " return None\n",
+ " plan = frontier.popleft() # sequence of HLA/Angelic HLA's \n",
+ " opt_reachable_set = Problem.reach_opt(problem.init, plan)\n",
+ " pes_reachable_set = Problem.reach_pes(problem.init, plan)\n",
+ " if problem.intersects_goal(opt_reachable_set): \n",
+ " if Problem.is_primitive( plan, hierarchy ): \n",
+ " return ([x for x in plan.action])\n",
+ " guaranteed = problem.intersects_goal(pes_reachable_set) \n",
+ " if guaranteed and Problem.making_progress(plan, initialPlan):\n",
+ " final_state = guaranteed[0] # any element of guaranteed \n",
+ " #print('decompose')\n",
+ " return Problem.decompose(hierarchy, problem, plan, final_state, pes_reachable_set)\n",
+ " (hla, index) = Problem.find_hla(plan, hierarchy) # there should be at least one HLA/Angelic_HLA, otherwise plan would be primitive.\n",
+ " prefix = plan.action[:index]\n",
+ " suffix = plan.action[index+1:]\n",
+ " outcome = Problem(Problem.result(problem.init, prefix), problem.goals , problem.actions )\n",
+ " for sequence in Problem.refinements(hla, outcome, hierarchy): # find refinements\n",
+ " frontier.append(Angelic_Node(outcome.init, plan, prefix + sequence+ suffix, prefix+sequence+suffix))\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(Problem.angelic_search)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "### Decompose \n",
+ "- Finds recursively the sequence of states and actions that lead us from initial state to goal.\n",
+ "- For each of the above actions we find their refinements,if they are not primitive, by calling the angelic_search function. \n",
+ " If there are not refinements return None\n",
+ " \n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " def decompose(hierarchy, s_0, plan, s_f, reachable_set):\n",
+ " solution = [] \n",
+ " i = max(reachable_set.keys())\n",
+ " while plan.action_pes: \n",
+ " action = plan.action_pes.pop()\n",
+ " if (i==0): \n",
+ " return solution\n",
+ " s_i = Problem.find_previous_state(s_f, reachable_set,i, action) \n",
+ " problem = Problem(s_i, s_f , plan.action)\n",
+ " angelic_call = Problem.angelic_search(problem, hierarchy, [Angelic_Node(s_i, Node(None), [action],[action])])\n",
+ " if angelic_call:\n",
+ " for x in angelic_call: \n",
+ " solution.insert(0,x)\n",
+ " else: \n",
+ " return None\n",
+ " s_f = s_i\n",
+ " i-=1\n",
+ " return solution\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(Problem.decompose)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Example\n",
+ "\n",
+ "Suppose that somebody wants to get to the airport. \n",
+ "The possible ways to do so is either get a taxi, or drive to the airport.
\n",
+ "Those two actions have some preconditions and some effects. \n",
+ "If you get the taxi, you need to have cash, whereas if you drive you need to have a car.
\n",
+ "Thus we define the following hierarchy of possible actions.\n",
+ "\n",
+ "##### hierarchy"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "library = {\n",
+ " 'HLA': ['Go(Home,SFO)', 'Go(Home,SFO)', 'Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)', 'Taxi(Home, SFO)'],\n",
+ " 'steps': [['Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)'], ['Taxi(Home, SFO)'], [], [], []],\n",
+ " 'precond': [['At(Home) & Have(Car)'], ['At(Home)'], ['At(Home) & Have(Car)'], ['At(SFOLongTermParking)'], ['At(Home)']],\n",
+ " 'effect': [['At(SFO) & ~At(Home)'], ['At(SFO) & ~At(Home) & ~Have(Cash)'], ['At(SFOLongTermParking) & ~At(Home)'], ['At(SFO) & ~At(LongTermParking)'], ['At(SFO) & ~At(Home) & ~Have(Cash)']] }\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "the possible actions are the following:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "go_SFO = HLA('Go(Home,SFO)', precond='At(Home)', effect='At(SFO) & ~At(Home)')\n",
+ "taxi_SFO = HLA('Taxi(Home,SFO)', precond='At(Home)', effect='At(SFO) & ~At(Home) & ~Have(Cash)')\n",
+ "drive_SFOLongTermParking = HLA('Drive(Home, SFOLongTermParking)', 'At(Home) & Have(Car)','At(SFOLongTermParking) & ~At(Home)' )\n",
+ "shuttle_SFO = HLA('Shuttle(SFOLongTermParking, SFO)', 'At(SFOLongTermParking)', 'At(SFO) & ~At(LongTermParking)')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Suppose that (our preconditionds are that) we are Home and we have cash and car and our goal is to get to SFO and maintain our cash, and our possible actions are the above.
\n",
+ "##### Then our problem is: "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "prob = Problem('At(Home) & Have(Cash) & Have(Car)', 'At(SFO) & Have(Cash)', [go_SFO, taxi_SFO, drive_SFOLongTermParking,shuttle_SFO])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "An agent gives us some approximate information about the plan we will follow:
\n",
+ "(initialPlan is an Angelic Node, where: \n",
+ "- state is the initial state of the problem, \n",
+ "- parent is None \n",
+ "- action: is a list of actions (Angelic HLA's) with the optimistic estimators of effects and \n",
+ "- action_pes: is a list of actions (Angelic HLA's) with the pessimistic approximations of the effects\n",
+ "##### InitialPlan"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "angelic_opt_description = Angelic_HLA('Go(Home, SFO)', precond = 'At(Home)', effect ='$+At(SFO) & $-At(Home)' ) \n",
+ "angelic_pes_description = Angelic_HLA('Go(Home, SFO)', precond = 'At(Home)', effect ='$+At(SFO) & ~At(Home)' )\n",
+ "\n",
+ "initialPlan = [Angelic_Node(prob.init, None, [angelic_opt_description], [angelic_pes_description])] \n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We want to find the optimistic and pessimistic reachable set of initialPlan when applied to the problem:\n",
+ "##### Optimistic/Pessimistic reachable set"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[[At(Home), Have(Cash), Have(Car)], [Have(Cash), Have(Car), At(SFO), NotAt(Home)], [Have(Cash), Have(Car), NotAt(Home)], [At(Home), Have(Cash), Have(Car), At(SFO)], [At(Home), Have(Cash), Have(Car)]] \n",
+ "\n",
+ "[[At(Home), Have(Cash), Have(Car)], [Have(Cash), Have(Car), At(SFO), NotAt(Home)], [Have(Cash), Have(Car), NotAt(Home)]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "opt_reachable_set = Problem.reach_opt(prob.init, initialPlan[0])\n",
+ "pes_reachable_set = Problem.reach_pes(prob.init, initialPlan[0])\n",
+ "print([x for y in opt_reachable_set.keys() for x in opt_reachable_set[y]], '\\n')\n",
+ "print([x for y in pes_reachable_set.keys() for x in pes_reachable_set[y]])\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "##### Refinements"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[HLA(Drive(Home, SFOLongTermParking)), HLA(Shuttle(SFOLongTermParking, SFO))]\n",
+ "[{'duration': 0, 'effect': [At(SFOLongTermParking), NotAt(Home)], 'args': (Home, SFOLongTermParking), 'uses': {}, 'consumes': {}, 'name': 'Drive', 'completed': False, 'precond': [At(Home), Have(Car)]}, {'duration': 0, 'effect': [At(SFO), NotAt(LongTermParking)], 'args': (SFOLongTermParking, SFO), 'uses': {}, 'consumes': {}, 'name': 'Shuttle', 'completed': False, 'precond': [At(SFOLongTermParking)]}] \n",
+ "\n",
+ "[HLA(Taxi(Home, SFO))]\n",
+ "[{'duration': 0, 'effect': [At(SFO), NotAt(Home), NotHave(Cash)], 'args': (Home, SFO), 'uses': {}, 'consumes': {}, 'name': 'Taxi', 'completed': False, 'precond': [At(Home)]}] \n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "for sequence in Problem.refinements(go_SFO, prob, library):\n",
+ " print (sequence)\n",
+ " print([x.__dict__ for x in sequence ], '\\n')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Run the angelic search\n",
+ "##### Top level call"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[HLA(Drive(Home, SFOLongTermParking)), HLA(Shuttle(SFOLongTermParking, SFO))] \n",
+ "\n",
+ "[{'duration': 0, 'effect': [At(SFOLongTermParking), NotAt(Home)], 'args': (Home, SFOLongTermParking), 'uses': {}, 'consumes': {}, 'name': 'Drive', 'completed': False, 'precond': [At(Home), Have(Car)]}, {'duration': 0, 'effect': [At(SFO), NotAt(LongTermParking)], 'args': (SFOLongTermParking, SFO), 'uses': {}, 'consumes': {}, 'name': 'Shuttle', 'completed': False, 'precond': [At(SFOLongTermParking)]}]\n"
+ ]
+ }
+ ],
+ "source": [
+ "plan= Problem.angelic_search(prob, library, initialPlan)\n",
+ "print (plan, '\\n')\n",
+ "print ([x.__dict__ for x in plan])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Example 2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "library_2 = {\n",
+ " 'HLA': ['Go(Home,SFO)', 'Go(Home,SFO)', 'Bus(Home, MetroStop)', 'Metro(MetroStop, SFO)' , 'Metro(MetroStop, SFO)', 'Metro1(MetroStop, SFO)', 'Metro2(MetroStop, SFO)' ,'Taxi(Home, SFO)'],\n",
+ " 'steps': [['Bus(Home, MetroStop)', 'Metro(MetroStop, SFO)'], ['Taxi(Home, SFO)'], [], ['Metro1(MetroStop, SFO)'], ['Metro2(MetroStop, SFO)'],[],[],[]],\n",
+ " 'precond': [['At(Home)'], ['At(Home)'], ['At(Home)'], ['At(MetroStop)'], ['At(MetroStop)'],['At(MetroStop)'], ['At(MetroStop)'] ,['At(Home) & Have(Cash)']],\n",
+ " 'effect': [['At(SFO) & ~At(Home)'], ['At(SFO) & ~At(Home) & ~Have(Cash)'], ['At(MetroStop) & ~At(Home)'], ['At(SFO) & ~At(MetroStop)'], ['At(SFO) & ~At(MetroStop)'], ['At(SFO) & ~At(MetroStop)'] , ['At(SFO) & ~At(MetroStop)'] ,['At(SFO) & ~At(Home) & ~Have(Cash)']] \n",
+ " }"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[HLA(Bus(Home, MetroStop)), HLA(Metro1(MetroStop, SFO))] \n",
+ "\n",
+ "[{'duration': 0, 'effect': [At(MetroStop), NotAt(Home)], 'args': (Home, MetroStop), 'uses': {}, 'consumes': {}, 'name': 'Bus', 'completed': False, 'precond': [At(Home)]}, {'duration': 0, 'effect': [At(SFO), NotAt(MetroStop)], 'args': (MetroStop, SFO), 'uses': {}, 'consumes': {}, 'name': 'Metro1', 'completed': False, 'precond': [At(MetroStop)]}]\n"
+ ]
+ }
+ ],
+ "source": [
+ "plan_2 = Problem.angelic_search(prob, library_2, initialPlan)\n",
+ "print(plan_2, '\\n')\n",
+ "print([x.__dict__ for x in plan_2])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Example 3 \n",
+ "\n",
+ "Sometimes there is no plan that achieves the goal!"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "library_3 = {\n",
+ " 'HLA': ['Shuttle(SFOLongTermParking, SFO)', 'Go(Home, SFOLongTermParking)', 'Taxi(Home, SFOLongTermParking)', 'Drive(Home, SFOLongTermParking)', 'Drive(SFOLongTermParking, Home)', 'Get(Cash)', 'Go(Home, ATM)'],\n",
+ " 'steps': [['Get(Cash)', 'Go(Home, SFOLongTermParking)'], ['Taxi(Home, SFOLongTermParking)'], [], [], [], ['Drive(SFOLongTermParking, Home)', 'Go(Home, ATM)'], []],\n",
+ " 'precond': [['At(SFOLongTermParking)'], ['At(Home)'], ['At(Home) & Have(Cash)'], ['At(Home)'], ['At(SFOLongTermParking)'], ['At(SFOLongTermParking)'], ['At(Home)']],\n",
+ " 'effect': [['At(SFO)'], ['At(SFO)'], ['At(SFOLongTermParking) & ~Have(Cash)'], ['At(SFOLongTermParking)'] ,['At(Home) & ~At(SFOLongTermParking)'], ['At(Home) & Have(Cash)'], ['Have(Cash)'] ]\n",
+ " }\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "shuttle_SFO = HLA('Shuttle(SFOLongTermParking, SFO)', 'Have(Cash) & At(SFOLongTermParking)', 'At(SFO)')\n",
+ "prob_3 = Problem('At(SFOLongTermParking) & Have(Cash)', 'At(SFO) & Have(Cash)', [shuttle_SFO])\n",
+ "# optimistic/pessimistic descriptions\n",
+ "angelic_opt_description = Angelic_HLA('Shuttle(SFOLongTermParking, SFO)', precond = 'At(SFOLongTermParking)', effect ='$+At(SFO) & $-At(SFOLongTermParking)' ) \n",
+ "angelic_pes_description = Angelic_HLA('Shuttle(SFOLongTermParking, SFO)', precond = 'At(SFOLongTermParking)', effect ='$+At(SFO) & ~At(SFOLongTermParking)' ) \n",
+ "# initial Plan\n",
+ "initialPlan_3 = [Angelic_Node(prob.init, None, [angelic_opt_description], [angelic_pes_description])] "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "None\n"
+ ]
+ }
+ ],
+ "source": [
+ "plan_3 = prob_3.angelic_search(library_3, initialPlan_3)\n",
+ "print(plan_3)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.5.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/planning_graphPlan.ipynb b/planning_graphPlan.ipynb
new file mode 100644
index 000000000..bffecb937
--- /dev/null
+++ b/planning_graphPlan.ipynb
@@ -0,0 +1,1066 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## SOLVING PLANNING PROBLEMS\n",
+ "----\n",
+ "### GRAPHPLAN\n",
+ "
\n",
+ "The GraphPlan algorithm is a popular method of solving classical planning problems.\n",
+ "Before we get into the details of the algorithm, let's look at a special data structure called **planning graph**, used to give better heuristic estimates and plays a key role in the GraphPlan algorithm."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Planning Graph\n",
+ "A planning graph is a directed graph organized into levels. \n",
+ "Each level contains information about the current state of the knowledge base and the possible state-action links to and from that level.\n",
+ "The first level contains the initial state with nodes representing each fluent that holds in that level.\n",
+ "This level has state-action links linking each state to valid actions in that state.\n",
+ "Each action is linked to all its preconditions and its effect states.\n",
+ "Based on these effects, the next level is constructed.\n",
+ "The next level contains similarly structured information about the next state.\n",
+ "In this way, the graph is expanded using state-action links till we reach a state where all the required goals hold true simultaneously.\n",
+ "We can say that we have reached our goal if none of the goal states in the current level are mutually exclusive.\n",
+ "This will be explained in detail later.\n",
+ "
\n",
+ "Planning graphs only work for propositional planning problems, hence we need to eliminate all variables by generating all possible substitutions.\n",
+ "
\n",
+ "For example, the planning graph of the `have_cake_and_eat_cake_too` problem might look like this\n",
+ "\n",
+ "
\n",
+ "The black lines indicate links between states and actions.\n",
+ "
\n",
+ "In every planning problem, we are allowed to carry out the `no-op` action, ie, we can choose no action for a particular state.\n",
+ "These are called 'Persistence' actions and are represented in the graph by the small square boxes.\n",
+ "In technical terms, a persistence action has effects same as its preconditions.\n",
+ "This enables us to carry a state to the next level.\n",
+ "
\n",
+ "
\n",
+ "The gray lines indicate mutual exclusivity.\n",
+ "This means that the actions connected bya gray line cannot be taken together.\n",
+ "Mutual exclusivity (mutex) occurs in the following cases:\n",
+ "1. **Inconsistent effects**: One action negates the effect of the other. For example, _Eat(Cake)_ and the persistence of _Have(Cake)_ have inconsistent effects because they disagree on the effect _Have(Cake)_\n",
+ "2. **Interference**: One of the effects of an action is the negation of a precondition of the other. For example, _Eat(Cake)_ interferes with the persistence of _Have(Cake)_ by negating its precondition.\n",
+ "3. **Competing needs**: One of the preconditions of one action is mutually exclusive with a precondition of the other. For example, _Bake(Cake)_ and _Eat(Cake)_ are mutex because they compete on the value of the _Have(Cake)_ precondition."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In the module, planning graphs have been implemented using two classes, `Level` which stores data for a particular level and `Graph` which connects multiple levels together.\n",
+ "Let's look at the `Level` class."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from planning import *\n",
+ "from notebook import psource"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "class Level:\n",
+ " """\n",
+ " Contains the state of the planning problem\n",
+ " and exhaustive list of actions which use the\n",
+ " states as pre-condition.\n",
+ " """\n",
+ "\n",
+ " def __init__(self, kb):\n",
+ " """Initializes variables to hold state and action details of a level"""\n",
+ "\n",
+ " self.kb = kb\n",
+ " # current state\n",
+ " self.current_state = kb.clauses\n",
+ " # current action to state link\n",
+ " self.current_action_links = {}\n",
+ " # current state to action link\n",
+ " self.current_state_links = {}\n",
+ " # current action to next state link\n",
+ " self.next_action_links = {}\n",
+ " # next state to current action link\n",
+ " self.next_state_links = {}\n",
+ " # mutually exclusive actions\n",
+ " self.mutex = []\n",
+ "\n",
+ " def __call__(self, actions, objects):\n",
+ " self.build(actions, objects)\n",
+ " self.find_mutex()\n",
+ "\n",
+ " def separate(self, e):\n",
+ " """Separates an iterable of elements into positive and negative parts"""\n",
+ "\n",
+ " positive = []\n",
+ " negative = []\n",
+ " for clause in e:\n",
+ " if clause.op[:3] == 'Not':\n",
+ " negative.append(clause)\n",
+ " else:\n",
+ " positive.append(clause)\n",
+ " return positive, negative\n",
+ "\n",
+ " def find_mutex(self):\n",
+ " """Finds mutually exclusive actions"""\n",
+ "\n",
+ " # Inconsistent effects\n",
+ " pos_nsl, neg_nsl = self.separate(self.next_state_links)\n",
+ "\n",
+ " for negeff in neg_nsl:\n",
+ " new_negeff = Expr(negeff.op[3:], *negeff.args)\n",
+ " for poseff in pos_nsl:\n",
+ " if new_negeff == poseff:\n",
+ " for a in self.next_state_links[poseff]:\n",
+ " for b in self.next_state_links[negeff]:\n",
+ " if {a, b} not in self.mutex:\n",
+ " self.mutex.append({a, b})\n",
+ "\n",
+ " # Interference will be calculated with the last step\n",
+ " pos_csl, neg_csl = self.separate(self.current_state_links)\n",
+ "\n",
+ " # Competing needs\n",
+ " for posprecond in pos_csl:\n",
+ " for negprecond in neg_csl:\n",
+ " new_negprecond = Expr(negprecond.op[3:], *negprecond.args)\n",
+ " if new_negprecond == posprecond:\n",
+ " for a in self.current_state_links[posprecond]:\n",
+ " for b in self.current_state_links[negprecond]:\n",
+ " if {a, b} not in self.mutex:\n",
+ " self.mutex.append({a, b})\n",
+ "\n",
+ " # Inconsistent support\n",
+ " state_mutex = []\n",
+ " for pair in self.mutex:\n",
+ " next_state_0 = self.next_action_links[list(pair)[0]]\n",
+ " if len(pair) == 2:\n",
+ " next_state_1 = self.next_action_links[list(pair)[1]]\n",
+ " else:\n",
+ " next_state_1 = self.next_action_links[list(pair)[0]]\n",
+ " if (len(next_state_0) == 1) and (len(next_state_1) == 1):\n",
+ " state_mutex.append({next_state_0[0], next_state_1[0]})\n",
+ " \n",
+ " self.mutex = self.mutex + state_mutex\n",
+ "\n",
+ " def build(self, actions, objects):\n",
+ " """Populates the lists and dictionaries containing the state action dependencies"""\n",
+ "\n",
+ " for clause in self.current_state:\n",
+ " p_expr = Expr('P' + clause.op, *clause.args)\n",
+ " self.current_action_links[p_expr] = [clause]\n",
+ " self.next_action_links[p_expr] = [clause]\n",
+ " self.current_state_links[clause] = [p_expr]\n",
+ " self.next_state_links[clause] = [p_expr]\n",
+ "\n",
+ " for a in actions:\n",
+ " num_args = len(a.args)\n",
+ " possible_args = tuple(itertools.permutations(objects, num_args))\n",
+ "\n",
+ " for arg in possible_args:\n",
+ " if a.check_precond(self.kb, arg):\n",
+ " for num, symbol in enumerate(a.args):\n",
+ " if not symbol.op.islower():\n",
+ " arg = list(arg)\n",
+ " arg[num] = symbol\n",
+ " arg = tuple(arg)\n",
+ "\n",
+ " new_action = a.substitute(Expr(a.name, *a.args), arg)\n",
+ " self.current_action_links[new_action] = []\n",
+ "\n",
+ " for clause in a.precond:\n",
+ " new_clause = a.substitute(clause, arg)\n",
+ " self.current_action_links[new_action].append(new_clause)\n",
+ " if new_clause in self.current_state_links:\n",
+ " self.current_state_links[new_clause].append(new_action)\n",
+ " else:\n",
+ " self.current_state_links[new_clause] = [new_action]\n",
+ " \n",
+ " self.next_action_links[new_action] = []\n",
+ " for clause in a.effect:\n",
+ " new_clause = a.substitute(clause, arg)\n",
+ "\n",
+ " self.next_action_links[new_action].append(new_clause)\n",
+ " if new_clause in self.next_state_links:\n",
+ " self.next_state_links[new_clause].append(new_action)\n",
+ " else:\n",
+ " self.next_state_links[new_clause] = [new_action]\n",
+ "\n",
+ " def perform_actions(self):\n",
+ " """Performs the necessary actions and returns a new Level"""\n",
+ "\n",
+ " new_kb = FolKB(list(set(self.next_state_links.keys())))\n",
+ " return Level(new_kb)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(Level)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Each level stores the following data\n",
+ "1. The current state of the level in `current_state`\n",
+ "2. Links from an action to its preconditions in `current_action_links`\n",
+ "3. Links from a state to the possible actions in that state in `current_state_links`\n",
+ "4. Links from each action to its effects in `next_action_links`\n",
+ "5. Links from each possible next state from each action in `next_state_links`. This stores the same information as the `current_action_links` of the next level.\n",
+ "6. Mutex links in `mutex`.\n",
+ "
\n",
+ "
\n",
+ "The `find_mutex` method finds the mutex links according to the points given above.\n",
+ "
\n",
+ "The `build` method populates the data structures storing the state and action information.\n",
+ "Persistence actions for each clause in the current state are also defined here. \n",
+ "The newly created persistence action has the same name as its state, prefixed with a 'P'."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's now look at the `Graph` class."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "class Graph:\n",
+ " """\n",
+ " Contains levels of state and actions\n",
+ " Used in graph planning algorithm to extract a solution\n",
+ " """\n",
+ "\n",
+ " def __init__(self, planningproblem):\n",
+ " self.planningproblem = planningproblem\n",
+ " self.kb = FolKB(planningproblem.init)\n",
+ " self.levels = [Level(self.kb)]\n",
+ " self.objects = set(arg for clause in self.kb.clauses for arg in clause.args)\n",
+ "\n",
+ " def __call__(self):\n",
+ " self.expand_graph()\n",
+ "\n",
+ " def expand_graph(self):\n",
+ " """Expands the graph by a level"""\n",
+ "\n",
+ " last_level = self.levels[-1]\n",
+ " last_level(self.planningproblem.actions, self.objects)\n",
+ " self.levels.append(last_level.perform_actions())\n",
+ "\n",
+ " def non_mutex_goals(self, goals, index):\n",
+ " """Checks whether the goals are mutually exclusive"""\n",
+ "\n",
+ " goal_perm = itertools.combinations(goals, 2)\n",
+ " for g in goal_perm:\n",
+ " if set(g) in self.levels[index].mutex:\n",
+ " return False\n",
+ " return True\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(Graph)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The class stores a problem definition in `pddl`, \n",
+ "a knowledge base in `kb`, \n",
+ "a list of `Level` objects in `levels` and \n",
+ "all the possible arguments found in the initial state of the problem in `objects`.\n",
+ "
\n",
+ "The `expand_graph` method generates a new level of the graph.\n",
+ "This method is invoked when the goal conditions haven't been met in the current level or the actions that lead to it are mutually exclusive.\n",
+ "The `non_mutex_goals` method checks whether the goals in the current state are mutually exclusive.\n",
+ "
\n",
+ "
\n",
+ "Using these two classes, we can define a planning graph which can either be used to provide reliable heuristics for planning problems or used in the `GraphPlan` algorithm.\n",
+ "
\n",
+ "Let's have a look at the `GraphPlan` class."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "class GraphPlan:\n",
+ " """\n",
+ " Class for formulation GraphPlan algorithm\n",
+ " Constructs a graph of state and action space\n",
+ " Returns solution for the planning problem\n",
+ " """\n",
+ "\n",
+ " def __init__(self, planningproblem):\n",
+ " self.graph = Graph(planningproblem)\n",
+ " self.nogoods = []\n",
+ " self.solution = []\n",
+ "\n",
+ " def check_leveloff(self):\n",
+ " """Checks if the graph has levelled off"""\n",
+ "\n",
+ " check = (set(self.graph.levels[-1].current_state) == set(self.graph.levels[-2].current_state))\n",
+ "\n",
+ " if check:\n",
+ " return True\n",
+ "\n",
+ " def extract_solution(self, goals, index):\n",
+ " """Extracts the solution"""\n",
+ "\n",
+ " level = self.graph.levels[index] \n",
+ " if not self.graph.non_mutex_goals(goals, index):\n",
+ " self.nogoods.append((level, goals))\n",
+ " return\n",
+ "\n",
+ " level = self.graph.levels[index - 1] \n",
+ "\n",
+ " # Create all combinations of actions that satisfy the goal \n",
+ " actions = []\n",
+ " for goal in goals:\n",
+ " actions.append(level.next_state_links[goal]) \n",
+ "\n",
+ " all_actions = list(itertools.product(*actions)) \n",
+ "\n",
+ " # Filter out non-mutex actions\n",
+ " non_mutex_actions = [] \n",
+ " for action_tuple in all_actions:\n",
+ " action_pairs = itertools.combinations(list(set(action_tuple)), 2) \n",
+ " non_mutex_actions.append(list(set(action_tuple))) \n",
+ " for pair in action_pairs: \n",
+ " if set(pair) in level.mutex:\n",
+ " non_mutex_actions.pop(-1)\n",
+ " break\n",
+ " \n",
+ "\n",
+ " # Recursion\n",
+ " for action_list in non_mutex_actions: \n",
+ " if [action_list, index] not in self.solution:\n",
+ " self.solution.append([action_list, index])\n",
+ "\n",
+ " new_goals = []\n",
+ " for act in set(action_list): \n",
+ " if act in level.current_action_links:\n",
+ " new_goals = new_goals + level.current_action_links[act]\n",
+ "\n",
+ " if abs(index) + 1 == len(self.graph.levels):\n",
+ " return\n",
+ " elif (level, new_goals) in self.nogoods:\n",
+ " return\n",
+ " else:\n",
+ " self.extract_solution(new_goals, index - 1)\n",
+ "\n",
+ " # Level-Order multiple solutions\n",
+ " solution = []\n",
+ " for item in self.solution:\n",
+ " if item[1] == -1:\n",
+ " solution.append([])\n",
+ " solution[-1].append(item[0])\n",
+ " else:\n",
+ " solution[-1].append(item[0])\n",
+ "\n",
+ " for num, item in enumerate(solution):\n",
+ " item.reverse()\n",
+ " solution[num] = item\n",
+ "\n",
+ " return solution\n",
+ "\n",
+ " def goal_test(self, kb):\n",
+ " return all(kb.ask(q) is not False for q in self.graph.planningproblem.goals)\n",
+ "\n",
+ " def execute(self):\n",
+ " """Executes the GraphPlan algorithm for the given problem"""\n",
+ "\n",
+ " while True:\n",
+ " self.graph.expand_graph()\n",
+ " if (self.goal_test(self.graph.levels[-1].kb) and self.graph.non_mutex_goals(self.graph.planningproblem.goals, -1)):\n",
+ " solution = self.extract_solution(self.graph.planningproblem.goals, -1)\n",
+ " if solution:\n",
+ " return solution\n",
+ " \n",
+ " if len(self.graph.levels) >= 2 and self.check_leveloff():\n",
+ " return None\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(GraphPlan)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Given a planning problem defined as a PlanningProblem, `GraphPlan` creates a planning graph stored in `graph` and expands it till it reaches a state where all its required goals are present simultaneously without mutual exclusivity.\n",
+ "
\n",
+ "Once a goal is found, `extract_solution` is called.\n",
+ "This method recursively finds the path to a solution given a planning graph.\n",
+ "In the case where `extract_solution` fails to find a solution for a set of goals as a given level, we record the `(level, goals)` pair as a **no-good**.\n",
+ "Whenever `extract_solution` is called again with the same level and goals, we can find the recorded no-good and immediately return failure rather than searching again. \n",
+ "No-goods are also used in the termination test.\n",
+ "
\n",
+ "The `check_leveloff` method checks if the planning graph for the problem has **levelled-off**, ie, it has the same states, actions and mutex pairs as the previous level.\n",
+ "If the graph has already levelled off and we haven't found a solution, there is no point expanding the graph, as it won't lead to anything new.\n",
+ "In such a case, we can declare that the planning problem is unsolvable with the given constraints.\n",
+ "
\n",
+ "
\n",
+ "To summarize, the `GraphPlan` algorithm calls `expand_graph` and tests whether it has reached the goal and if the goals are non-mutex.\n",
+ "
\n",
+ "If so, `extract_solution` is invoked which recursively reconstructs the solution from the planning graph.\n",
+ "
\n",
+ "If not, then we check if our graph has levelled off and continue if it hasn't."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's solve a few planning problems that we had defined earlier."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Air cargo problem\n",
+ "In accordance with the summary above, we have defined a helper function to carry out `GraphPlan` on the `air_cargo` problem.\n",
+ "The function is pretty straightforward.\n",
+ "Let's have a look."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def air_cargo_graphplan():\n",
+ " """Solves the air cargo problem using GraphPlan"""\n",
+ " return GraphPlan(air_cargo()).execute()\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(air_cargo_graphplan)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let's instantiate the problem and find a solution using this helper function."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[[[Load(C2, P2, JFK),\n",
+ " PAirport(SFO),\n",
+ " PAirport(JFK),\n",
+ " PPlane(P2),\n",
+ " PPlane(P1),\n",
+ " Fly(P2, JFK, SFO),\n",
+ " PCargo(C2),\n",
+ " Load(C1, P1, SFO),\n",
+ " Fly(P1, SFO, JFK),\n",
+ " PCargo(C1)],\n",
+ " [Unload(C2, P2, SFO), Unload(C1, P1, JFK)]]]"
+ ]
+ },
+ "execution_count": 19,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "airCargoG = air_cargo_graphplan()\n",
+ "airCargoG"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Each element in the solution is a valid action.\n",
+ "The solution is separated into lists for each level.\n",
+ "The actions prefixed with a 'P' are persistence actions and can be ignored.\n",
+ "They simply carry certain states forward.\n",
+ "We have another helper function `linearize` that presents the solution in a more readable format, much like a total-order planner, but it is _not_ a total-order planner."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[Load(C2, P2, JFK),\n",
+ " Fly(P2, JFK, SFO),\n",
+ " Load(C1, P1, SFO),\n",
+ " Fly(P1, SFO, JFK),\n",
+ " Unload(C2, P2, SFO),\n",
+ " Unload(C1, P1, JFK)]"
+ ]
+ },
+ "execution_count": 20,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "linearize(airCargoG)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Indeed, this is a correct solution.\n",
+ "
\n",
+ "There are similar helper functions for some other planning problems.\n",
+ "
\n",
+ "Lets' try solving the spare tire problem."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[Remove(Spare, Trunk), Remove(Flat, Axle), PutOn(Spare, Axle)]"
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "spareTireG = spare_tire_graphplan()\n",
+ "linearize(spareTireG)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Solution for the cake problem"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[Eat(Cake), Bake(Cake)]"
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "cakeProblemG = have_cake_and_eat_cake_too_graphplan()\n",
+ "linearize(cakeProblemG)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Solution for the Sussman's Anomaly configuration of three blocks."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]"
+ ]
+ },
+ "execution_count": 23,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "sussmanAnomalyG = three_block_tower_graphplan()\n",
+ "linearize(sussmanAnomalyG)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Solution of the socks and shoes problem"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[RightSock, LeftSock, RightShoe, LeftShoe]"
+ ]
+ },
+ "execution_count": 24,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "socksShoesG = socks_and_shoes_graphplan()\n",
+ "linearize(socksShoesG)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.5.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/planning_hierarchical_search.ipynb b/planning_hierarchical_search.ipynb
new file mode 100644
index 000000000..18e57b23b
--- /dev/null
+++ b/planning_hierarchical_search.ipynb
@@ -0,0 +1,546 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Hierarchical Search \n",
+ "\n",
+ "Hierarchical search is a a planning algorithm in high level of abstraction.
\n",
+ "Instead of actions as in classical planning (chapter 10) (primitive actions) we now use high level actions (HLAs) (see planning.ipynb)
\n",
+ "\n",
+ "## Refinements\n",
+ "\n",
+ "Each __HLA__ has one or more refinements into a sequence of actions, each of which may be an HLA or a primitive action (which has no refinements by definition).
\n",
+ "For example:\n",
+ "- (a) the high level action \"Go to San Fransisco airport\" (Go(Home, SFO)), might have two possible refinements, \"Drive to San Fransisco airport\" and \"Taxi to San Fransisco airport\". \n",
+ "
\n",
+ "- (b) A recursive refinement for navigation in the vacuum world would be: to get to a\n",
+ "destination, take a step, and then go to the destination.\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "- __implementation__: An HLA refinement that contains only primitive actions is called an implementation of the HLA\n",
+ "- An implementation of a high-level plan (a sequence of HLAs) is the concatenation of implementations of each HLA in the sequence\n",
+ "- A high-level plan __achieves the goal__ from a given state if at least one of its implementations achieves the goal from that state\n",
+ "
\n",
+ "\n",
+ "The refinements function input is: \n",
+ "- __hla__: the HLA of which we want to compute its refinements\n",
+ "- __state__: the knoweledge base of the current problem (Problem.init)\n",
+ "- __library__: the hierarchy of the actions in the planning problem\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from planning import * \n",
+ "from notebook import psource"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " def refinements(hla, state, library): # refinements may be (multiple) HLA themselves ...\n",
+ " """\n",
+ " state is a Problem, containing the current state kb\n",
+ " library is a dictionary containing details for every possible refinement. eg:\n",
+ " {\n",
+ " 'HLA': [\n",
+ " 'Go(Home, SFO)',\n",
+ " 'Go(Home, SFO)',\n",
+ " 'Drive(Home, SFOLongTermParking)',\n",
+ " 'Shuttle(SFOLongTermParking, SFO)',\n",
+ " 'Taxi(Home, SFO)'\n",
+ " ],\n",
+ " 'steps': [\n",
+ " ['Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)'],\n",
+ " ['Taxi(Home, SFO)'],\n",
+ " [],\n",
+ " [],\n",
+ " []\n",
+ " ],\n",
+ " # empty refinements indicate a primitive action\n",
+ " 'precond': [\n",
+ " ['At(Home) & Have(Car)'],\n",
+ " ['At(Home)'],\n",
+ " ['At(Home) & Have(Car)'],\n",
+ " ['At(SFOLongTermParking)'],\n",
+ " ['At(Home)']\n",
+ " ],\n",
+ " 'effect': [\n",
+ " ['At(SFO) & ~At(Home)'],\n",
+ " ['At(SFO) & ~At(Home)'],\n",
+ " ['At(SFOLongTermParking) & ~At(Home)'],\n",
+ " ['At(SFO) & ~At(SFOLongTermParking)'],\n",
+ " ['At(SFO) & ~At(Home)']\n",
+ " ]\n",
+ " }\n",
+ " """\n",
+ " e = Expr(hla.name, hla.args)\n",
+ " indices = [i for i, x in enumerate(library['HLA']) if expr(x).op == hla.name]\n",
+ " for i in indices:\n",
+ " actions = []\n",
+ " for j in range(len(library['steps'][i])):\n",
+ " # find the index of the step [j] of the HLA \n",
+ " index_step = [k for k,x in enumerate(library['HLA']) if x == library['steps'][i][j]][0]\n",
+ " precond = library['precond'][index_step][0] # preconditions of step [j]\n",
+ " effect = library['effect'][index_step][0] # effect of step [j]\n",
+ " actions.append(HLA(library['steps'][i][j], precond, effect))\n",
+ " yield actions\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(Problem.refinements)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Hierarchical search \n",
+ "\n",
+ "Hierarchical search is a breadth-first implementation of hierarchical forward planning search in the space of refinements. (i.e. repeatedly choose an HLA in the current plan and replace it with one of its refinements, until the plan achieves the goal.) \n",
+ "\n",
+ "
\n",
+ "The algorithms input is: problem and hierarchy\n",
+ "- __problem__: is of type Problem \n",
+ "- __hierarchy__: is a dictionary consisting of all the actions and the order in which they are performed. \n",
+ "
\n",
+ "\n",
+ "In top level call, initialPlan contains [act] (i.e. is the action to be performed) "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " def hierarchical_search(problem, hierarchy):\n",
+ " """\n",
+ " [Figure 11.5] 'Hierarchical Search, a Breadth First Search implementation of Hierarchical\n",
+ " Forward Planning Search'\n",
+ " The problem is a real-world problem defined by the problem class, and the hierarchy is\n",
+ " a dictionary of HLA - refinements (see refinements generator for details)\n",
+ " """\n",
+ " act = Node(problem.init, None, [problem.actions[0]])\n",
+ " frontier = deque()\n",
+ " frontier.append(act)\n",
+ " while True:\n",
+ " if not frontier:\n",
+ " return None\n",
+ " plan = frontier.popleft()\n",
+ " (hla, index) = Problem.find_hla(plan, hierarchy) # finds the first non primitive hla in plan actions\n",
+ " prefix = plan.action[:index]\n",
+ " outcome = Problem(Problem.result(problem.init, prefix), problem.goals , problem.actions )\n",
+ " suffix = plan.action[index+1:]\n",
+ " if not hla: # hla is None and plan is primitive\n",
+ " if outcome.goal_test():\n",
+ " return plan.action\n",
+ " else:\n",
+ " for sequence in Problem.refinements(hla, outcome, hierarchy): # find refinements\n",
+ " frontier.append(Node(outcome.init, plan, prefix + sequence+ suffix))\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(Problem.hierarchical_search)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Example\n",
+ "\n",
+ "Suppose that somebody wants to get to the airport. \n",
+ "The possible ways to do so is either get a taxi, or drive to the airport.
\n",
+ "Those two actions have some preconditions and some effects. \n",
+ "If you get the taxi, you need to have cash, whereas if you drive you need to have a car.
\n",
+ "Thus we define the following hierarchy of possible actions.\n",
+ "\n",
+ "##### hierarchy"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "library = {\n",
+ " 'HLA': ['Go(Home,SFO)', 'Go(Home,SFO)', 'Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)', 'Taxi(Home, SFO)'],\n",
+ " 'steps': [['Drive(Home, SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)'], ['Taxi(Home, SFO)'], [], [], []],\n",
+ " 'precond': [['At(Home) & Have(Car)'], ['At(Home)'], ['At(Home) & Have(Car)'], ['At(SFOLongTermParking)'], ['At(Home)']],\n",
+ " 'effect': [['At(SFO) & ~At(Home)'], ['At(SFO) & ~At(Home) & ~Have(Cash)'], ['At(SFOLongTermParking) & ~At(Home)'], ['At(SFO) & ~At(LongTermParking)'], ['At(SFO) & ~At(Home) & ~Have(Cash)']] }\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "the possible actions are the following:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "go_SFO = HLA('Go(Home,SFO)', precond='At(Home)', effect='At(SFO) & ~At(Home)')\n",
+ "taxi_SFO = HLA('Taxi(Home,SFO)', precond='At(Home)', effect='At(SFO) & ~At(Home) & ~Have(Cash)')\n",
+ "drive_SFOLongTermParking = HLA('Drive(Home, SFOLongTermParking)', 'At(Home) & Have(Car)','At(SFOLongTermParking) & ~At(Home)' )\n",
+ "shuttle_SFO = HLA('Shuttle(SFOLongTermParking, SFO)', 'At(SFOLongTermParking)', 'At(SFO) & ~At(LongTermParking)')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Suppose that (our preconditionds are that) we are Home and we have cash and car and our goal is to get to SFO and maintain our cash, and our possible actions are the above.
\n",
+ "##### Then our problem is: "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "prob = Problem('At(Home) & Have(Cash) & Have(Car)', 'At(SFO) & Have(Cash)', [go_SFO])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "##### Refinements\n",
+ "\n",
+ "The refinements of the action Go(Home, SFO), are defined as:
\n",
+ "['Drive(Home,SFOLongTermParking)', 'Shuttle(SFOLongTermParking, SFO)'], ['Taxi(Home, SFO)']"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[HLA(Drive(Home, SFOLongTermParking)), HLA(Shuttle(SFOLongTermParking, SFO))]\n",
+ "[{'completed': False, 'args': (Home, SFOLongTermParking), 'name': 'Drive', 'uses': {}, 'duration': 0, 'effect': [At(SFOLongTermParking), NotAt(Home)], 'consumes': {}, 'precond': [At(Home), Have(Car)]}, {'completed': False, 'args': (SFOLongTermParking, SFO), 'name': 'Shuttle', 'uses': {}, 'duration': 0, 'effect': [At(SFO), NotAt(LongTermParking)], 'consumes': {}, 'precond': [At(SFOLongTermParking)]}] \n",
+ "\n",
+ "[HLA(Taxi(Home, SFO))]\n",
+ "[{'completed': False, 'args': (Home, SFO), 'name': 'Taxi', 'uses': {}, 'duration': 0, 'effect': [At(SFO), NotAt(Home), NotHave(Cash)], 'consumes': {}, 'precond': [At(Home)]}] \n",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "for sequence in Problem.refinements(go_SFO, prob, library):\n",
+ " print (sequence)\n",
+ " print([x.__dict__ for x in sequence ], '\\n')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Run the hierarchical search\n",
+ "##### Top level call"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[HLA(Drive(Home, SFOLongTermParking)), HLA(Shuttle(SFOLongTermParking, SFO))] \n",
+ "\n",
+ "[{'completed': False, 'args': (Home, SFOLongTermParking), 'name': 'Drive', 'uses': {}, 'duration': 0, 'effect': [At(SFOLongTermParking), NotAt(Home)], 'consumes': {}, 'precond': [At(Home), Have(Car)]}, {'completed': False, 'args': (SFOLongTermParking, SFO), 'name': 'Shuttle', 'uses': {}, 'duration': 0, 'effect': [At(SFO), NotAt(LongTermParking)], 'consumes': {}, 'precond': [At(SFOLongTermParking)]}]\n"
+ ]
+ }
+ ],
+ "source": [
+ "plan= Problem.hierarchical_search(prob, library)\n",
+ "print (plan, '\\n')\n",
+ "print ([x.__dict__ for x in plan])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Example 2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "library_2 = {\n",
+ " 'HLA': ['Go(Home,SFO)', 'Go(Home,SFO)', 'Bus(Home, MetroStop)', 'Metro(MetroStop, SFO)' , 'Metro(MetroStop, SFO)', 'Metro1(MetroStop, SFO)', 'Metro2(MetroStop, SFO)' ,'Taxi(Home, SFO)'],\n",
+ " 'steps': [['Bus(Home, MetroStop)', 'Metro(MetroStop, SFO)'], ['Taxi(Home, SFO)'], [], ['Metro1(MetroStop, SFO)'], ['Metro2(MetroStop, SFO)'],[],[],[]],\n",
+ " 'precond': [['At(Home)'], ['At(Home)'], ['At(Home)'], ['At(MetroStop)'], ['At(MetroStop)'],['At(MetroStop)'], ['At(MetroStop)'] ,['At(Home) & Have(Cash)']],\n",
+ " 'effect': [['At(SFO) & ~At(Home)'], ['At(SFO) & ~At(Home) & ~Have(Cash)'], ['At(MetroStop) & ~At(Home)'], ['At(SFO) & ~At(MetroStop)'], ['At(SFO) & ~At(MetroStop)'], ['At(SFO) & ~At(MetroStop)'] , ['At(SFO) & ~At(MetroStop)'] ,['At(SFO) & ~At(Home) & ~Have(Cash)']] \n",
+ " }"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[HLA(Bus(Home, MetroStop)), HLA(Metro1(MetroStop, SFO))] \n",
+ "\n",
+ "[{'completed': False, 'args': (Home, MetroStop), 'name': 'Bus', 'uses': {}, 'duration': 0, 'effect': [At(MetroStop), NotAt(Home)], 'consumes': {}, 'precond': [At(Home)]}, {'completed': False, 'args': (MetroStop, SFO), 'name': 'Metro1', 'uses': {}, 'duration': 0, 'effect': [At(SFO), NotAt(MetroStop)], 'consumes': {}, 'precond': [At(MetroStop)]}]\n"
+ ]
+ }
+ ],
+ "source": [
+ "plan_2 = Problem.hierarchical_search(prob, library_2)\n",
+ "print(plan_2, '\\n')\n",
+ "print([x.__dict__ for x in plan_2])"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.5.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/planning_partial_order_planner.ipynb b/planning_partial_order_planner.ipynb
new file mode 100644
index 000000000..4b1a98bb3
--- /dev/null
+++ b/planning_partial_order_planner.ipynb
@@ -0,0 +1,850 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### PARTIAL ORDER PLANNER\n",
+ "A partial-order planning algorithm is significantly different from a total-order planner.\n",
+ "The way a partial-order plan works enables it to take advantage of _problem decomposition_ and work on each subproblem separately.\n",
+ "It works on several subgoals independently, solves them with several subplans, and then combines the plan.\n",
+ "
\n",
+ "A partial-order planner also follows the **least commitment** strategy, where it delays making choices for as long as possible.\n",
+ "Variables are not bound unless it is absolutely necessary and new actions are chosen only if the existing actions cannot fulfil the required precondition.\n",
+ "
\n",
+ "Any planning algorithm that can place two actions into a plan without specifying which comes first is called a **partial-order planner**.\n",
+ "A partial-order planner searches through the space of plans rather than the space of states, which makes it perform better for certain problems.\n",
+ "
\n",
+ "
\n",
+ "Let's have a look at the `PartialOrderPlanner` class."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from planning import *\n",
+ "from notebook import psource"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "class PartialOrderPlanner:\n",
+ "\n",
+ " def __init__(self, planningproblem):\n",
+ " self.planningproblem = planningproblem\n",
+ " self.initialize()\n",
+ "\n",
+ " def initialize(self):\n",
+ " """Initialize all variables"""\n",
+ " self.causal_links = []\n",
+ " self.start = Action('Start', [], self.planningproblem.init)\n",
+ " self.finish = Action('Finish', self.planningproblem.goals, [])\n",
+ " self.actions = set()\n",
+ " self.actions.add(self.start)\n",
+ " self.actions.add(self.finish)\n",
+ " self.constraints = set()\n",
+ " self.constraints.add((self.start, self.finish))\n",
+ " self.agenda = set()\n",
+ " for precond in self.finish.precond:\n",
+ " self.agenda.add((precond, self.finish))\n",
+ " self.expanded_actions = self.expand_actions()\n",
+ "\n",
+ " def expand_actions(self, name=None):\n",
+ " """Generate all possible actions with variable bindings for precondition selection heuristic"""\n",
+ "\n",
+ " objects = set(arg for clause in self.planningproblem.init for arg in clause.args)\n",
+ " expansions = []\n",
+ " action_list = []\n",
+ " if name is not None:\n",
+ " for action in self.planningproblem.actions:\n",
+ " if str(action.name) == name:\n",
+ " action_list.append(action)\n",
+ " else:\n",
+ " action_list = self.planningproblem.actions\n",
+ "\n",
+ " for action in action_list:\n",
+ " for permutation in itertools.permutations(objects, len(action.args)):\n",
+ " bindings = unify(Expr(action.name, *action.args), Expr(action.name, *permutation))\n",
+ " if bindings is not None:\n",
+ " new_args = []\n",
+ " for arg in action.args:\n",
+ " if arg in bindings:\n",
+ " new_args.append(bindings[arg])\n",
+ " else:\n",
+ " new_args.append(arg)\n",
+ " new_expr = Expr(str(action.name), *new_args)\n",
+ " new_preconds = []\n",
+ " for precond in action.precond:\n",
+ " new_precond_args = []\n",
+ " for arg in precond.args:\n",
+ " if arg in bindings:\n",
+ " new_precond_args.append(bindings[arg])\n",
+ " else:\n",
+ " new_precond_args.append(arg)\n",
+ " new_precond = Expr(str(precond.op), *new_precond_args)\n",
+ " new_preconds.append(new_precond)\n",
+ " new_effects = []\n",
+ " for effect in action.effect:\n",
+ " new_effect_args = []\n",
+ " for arg in effect.args:\n",
+ " if arg in bindings:\n",
+ " new_effect_args.append(bindings[arg])\n",
+ " else:\n",
+ " new_effect_args.append(arg)\n",
+ " new_effect = Expr(str(effect.op), *new_effect_args)\n",
+ " new_effects.append(new_effect)\n",
+ " expansions.append(Action(new_expr, new_preconds, new_effects))\n",
+ "\n",
+ " return expansions\n",
+ "\n",
+ " def find_open_precondition(self):\n",
+ " """Find open precondition with the least number of possible actions"""\n",
+ "\n",
+ " number_of_ways = dict()\n",
+ " actions_for_precondition = dict()\n",
+ " for element in self.agenda:\n",
+ " open_precondition = element[0]\n",
+ " possible_actions = list(self.actions) + self.expanded_actions\n",
+ " for action in possible_actions:\n",
+ " for effect in action.effect:\n",
+ " if effect == open_precondition:\n",
+ " if open_precondition in number_of_ways:\n",
+ " number_of_ways[open_precondition] += 1\n",
+ " actions_for_precondition[open_precondition].append(action)\n",
+ " else:\n",
+ " number_of_ways[open_precondition] = 1\n",
+ " actions_for_precondition[open_precondition] = [action]\n",
+ "\n",
+ " number = sorted(number_of_ways, key=number_of_ways.__getitem__)\n",
+ " \n",
+ " for k, v in number_of_ways.items():\n",
+ " if v == 0:\n",
+ " return None, None, None\n",
+ "\n",
+ " act1 = None\n",
+ " for element in self.agenda:\n",
+ " if element[0] == number[0]:\n",
+ " act1 = element[1]\n",
+ " break\n",
+ "\n",
+ " if number[0] in self.expanded_actions:\n",
+ " self.expanded_actions.remove(number[0])\n",
+ "\n",
+ " return number[0], act1, actions_for_precondition[number[0]]\n",
+ "\n",
+ " def find_action_for_precondition(self, oprec):\n",
+ " """Find action for a given precondition"""\n",
+ "\n",
+ " # either\n",
+ " # choose act0 E Actions such that act0 achieves G\n",
+ " for action in self.actions:\n",
+ " for effect in action.effect:\n",
+ " if effect == oprec:\n",
+ " return action, 0\n",
+ "\n",
+ " # or\n",
+ " # choose act0 E Actions such that act0 achieves G\n",
+ " for action in self.planningproblem.actions:\n",
+ " for effect in action.effect:\n",
+ " if effect.op == oprec.op:\n",
+ " bindings = unify(effect, oprec)\n",
+ " if bindings is None:\n",
+ " break\n",
+ " return action, bindings\n",
+ "\n",
+ " def generate_expr(self, clause, bindings):\n",
+ " """Generate atomic expression from generic expression given variable bindings"""\n",
+ "\n",
+ " new_args = []\n",
+ " for arg in clause.args:\n",
+ " if arg in bindings:\n",
+ " new_args.append(bindings[arg])\n",
+ " else:\n",
+ " new_args.append(arg)\n",
+ "\n",
+ " try:\n",
+ " return Expr(str(clause.name), *new_args)\n",
+ " except:\n",
+ " return Expr(str(clause.op), *new_args)\n",
+ " \n",
+ " def generate_action_object(self, action, bindings):\n",
+ " """Generate action object given a generic action andvariable bindings"""\n",
+ "\n",
+ " # if bindings is 0, it means the action already exists in self.actions\n",
+ " if bindings == 0:\n",
+ " return action\n",
+ "\n",
+ " # bindings cannot be None\n",
+ " else:\n",
+ " new_expr = self.generate_expr(action, bindings)\n",
+ " new_preconds = []\n",
+ " for precond in action.precond:\n",
+ " new_precond = self.generate_expr(precond, bindings)\n",
+ " new_preconds.append(new_precond)\n",
+ " new_effects = []\n",
+ " for effect in action.effect:\n",
+ " new_effect = self.generate_expr(effect, bindings)\n",
+ " new_effects.append(new_effect)\n",
+ " return Action(new_expr, new_preconds, new_effects)\n",
+ "\n",
+ " def cyclic(self, graph):\n",
+ " """Check cyclicity of a directed graph"""\n",
+ "\n",
+ " new_graph = dict()\n",
+ " for element in graph:\n",
+ " if element[0] in new_graph:\n",
+ " new_graph[element[0]].append(element[1])\n",
+ " else:\n",
+ " new_graph[element[0]] = [element[1]]\n",
+ "\n",
+ " path = set()\n",
+ "\n",
+ " def visit(vertex):\n",
+ " path.add(vertex)\n",
+ " for neighbor in new_graph.get(vertex, ()):\n",
+ " if neighbor in path or visit(neighbor):\n",
+ " return True\n",
+ " path.remove(vertex)\n",
+ " return False\n",
+ "\n",
+ " value = any(visit(v) for v in new_graph)\n",
+ " return value\n",
+ "\n",
+ " def add_const(self, constraint, constraints):\n",
+ " """Add the constraint to constraints if the resulting graph is acyclic"""\n",
+ "\n",
+ " if constraint[0] == self.finish or constraint[1] == self.start:\n",
+ " return constraints\n",
+ "\n",
+ " new_constraints = set(constraints)\n",
+ " new_constraints.add(constraint)\n",
+ "\n",
+ " if self.cyclic(new_constraints):\n",
+ " return constraints\n",
+ " return new_constraints\n",
+ "\n",
+ " def is_a_threat(self, precondition, effect):\n",
+ " """Check if effect is a threat to precondition"""\n",
+ "\n",
+ " if (str(effect.op) == 'Not' + str(precondition.op)) or ('Not' + str(effect.op) == str(precondition.op)):\n",
+ " if effect.args == precondition.args:\n",
+ " return True\n",
+ " return False\n",
+ "\n",
+ " def protect(self, causal_link, action, constraints):\n",
+ " """Check and resolve threats by promotion or demotion"""\n",
+ "\n",
+ " threat = False\n",
+ " for effect in action.effect:\n",
+ " if self.is_a_threat(causal_link[1], effect):\n",
+ " threat = True\n",
+ " break\n",
+ "\n",
+ " if action != causal_link[0] and action != causal_link[2] and threat:\n",
+ " # try promotion\n",
+ " new_constraints = set(constraints)\n",
+ " new_constraints.add((action, causal_link[0]))\n",
+ " if not self.cyclic(new_constraints):\n",
+ " constraints = self.add_const((action, causal_link[0]), constraints)\n",
+ " else:\n",
+ " # try demotion\n",
+ " new_constraints = set(constraints)\n",
+ " new_constraints.add((causal_link[2], action))\n",
+ " if not self.cyclic(new_constraints):\n",
+ " constraints = self.add_const((causal_link[2], action), constraints)\n",
+ " else:\n",
+ " # both promotion and demotion fail\n",
+ " print('Unable to resolve a threat caused by', action, 'onto', causal_link)\n",
+ " return\n",
+ " return constraints\n",
+ "\n",
+ " def convert(self, constraints):\n",
+ " """Convert constraints into a dict of Action to set orderings"""\n",
+ "\n",
+ " graph = dict()\n",
+ " for constraint in constraints:\n",
+ " if constraint[0] in graph:\n",
+ " graph[constraint[0]].add(constraint[1])\n",
+ " else:\n",
+ " graph[constraint[0]] = set()\n",
+ " graph[constraint[0]].add(constraint[1])\n",
+ " return graph\n",
+ "\n",
+ " def toposort(self, graph):\n",
+ " """Generate topological ordering of constraints"""\n",
+ "\n",
+ " if len(graph) == 0:\n",
+ " return\n",
+ "\n",
+ " graph = graph.copy()\n",
+ "\n",
+ " for k, v in graph.items():\n",
+ " v.discard(k)\n",
+ "\n",
+ " extra_elements_in_dependencies = _reduce(set.union, graph.values()) - set(graph.keys())\n",
+ "\n",
+ " graph.update({element:set() for element in extra_elements_in_dependencies})\n",
+ " while True:\n",
+ " ordered = set(element for element, dependency in graph.items() if len(dependency) == 0)\n",
+ " if not ordered:\n",
+ " break\n",
+ " yield ordered\n",
+ " graph = {element: (dependency - ordered) for element, dependency in graph.items() if element not in ordered}\n",
+ " if len(graph) != 0:\n",
+ " raise ValueError('The graph is not acyclic and cannot be linearly ordered')\n",
+ "\n",
+ " def display_plan(self):\n",
+ " """Display causal links, constraints and the plan"""\n",
+ "\n",
+ " print('Causal Links')\n",
+ " for causal_link in self.causal_links:\n",
+ " print(causal_link)\n",
+ "\n",
+ " print('\\nConstraints')\n",
+ " for constraint in self.constraints:\n",
+ " print(constraint[0], '<', constraint[1])\n",
+ "\n",
+ " print('\\nPartial Order Plan')\n",
+ " print(list(reversed(list(self.toposort(self.convert(self.constraints))))))\n",
+ "\n",
+ " def execute(self, display=True):\n",
+ " """Execute the algorithm"""\n",
+ "\n",
+ " step = 1\n",
+ " self.tries = 1\n",
+ " while len(self.agenda) > 0:\n",
+ " step += 1\n",
+ " # select <G, act1> from Agenda\n",
+ " try:\n",
+ " G, act1, possible_actions = self.find_open_precondition()\n",
+ " except IndexError:\n",
+ " print('Probably Wrong')\n",
+ " break\n",
+ "\n",
+ " act0 = possible_actions[0]\n",
+ " # remove <G, act1> from Agenda\n",
+ " self.agenda.remove((G, act1))\n",
+ "\n",
+ " # For actions with variable number of arguments, use least commitment principle\n",
+ " # act0_temp, bindings = self.find_action_for_precondition(G)\n",
+ " # act0 = self.generate_action_object(act0_temp, bindings)\n",
+ "\n",
+ " # Actions = Actions U {act0}\n",
+ " self.actions.add(act0)\n",
+ "\n",
+ " # Constraints = add_const(start < act0, Constraints)\n",
+ " self.constraints = self.add_const((self.start, act0), self.constraints)\n",
+ "\n",
+ " # for each CL E CausalLinks do\n",
+ " # Constraints = protect(CL, act0, Constraints)\n",
+ " for causal_link in self.causal_links:\n",
+ " self.constraints = self.protect(causal_link, act0, self.constraints)\n",
+ "\n",
+ " # Agenda = Agenda U {<P, act0>: P is a precondition of act0}\n",
+ " for precondition in act0.precond:\n",
+ " self.agenda.add((precondition, act0))\n",
+ "\n",
+ " # Constraints = add_const(act0 < act1, Constraints)\n",
+ " self.constraints = self.add_const((act0, act1), self.constraints)\n",
+ "\n",
+ " # CausalLinks U {<act0, G, act1>}\n",
+ " if (act0, G, act1) not in self.causal_links:\n",
+ " self.causal_links.append((act0, G, act1))\n",
+ "\n",
+ " # for each A E Actions do\n",
+ " # Constraints = protect(<act0, G, act1>, A, Constraints)\n",
+ " for action in self.actions:\n",
+ " self.constraints = self.protect((act0, G, act1), action, self.constraints)\n",
+ "\n",
+ " if step > 200:\n",
+ " print('Couldn\\'t find a solution')\n",
+ " return None, None\n",
+ "\n",
+ " if display:\n",
+ " self.display_plan()\n",
+ " else:\n",
+ " return self.constraints, self.causal_links \n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(PartialOrderPlanner)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We will first describe the data-structures and helper methods used, followed by the algorithm used to find a partial-order plan."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Each plan has the following four components:\n",
+ "\n",
+ "1. **`actions`**: a set of actions that make up the steps of the plan.\n",
+ "`actions` is always a subset of `pddl.actions` the set of possible actions for the given planning problem. \n",
+ "The `start` and `finish` actions are dummy actions defined to bring uniformity to the problem. The `start` action has no preconditions and its effects constitute the initial state of the planning problem. \n",
+ "The `finish` action has no effects and its preconditions constitute the goal state of the planning problem.\n",
+ "The empty plan consists of just these two dummy actions.\n",
+ "2. **`constraints`**: a set of temporal constraints that define the order of performing the actions relative to each other.\n",
+ "`constraints` does not define a linear ordering, rather it usually represents a directed graph which is also acyclic if the plan is consistent.\n",
+ "Each ordering is of the form A < B, which reads as \"A before B\" and means that action A _must_ be executed sometime before action B, but not necessarily immediately before.\n",
+ "`constraints` stores these as a set of tuples `(Action(A), Action(B))` which is interpreted as given above.\n",
+ "A constraint cannot be added to `constraints` if it breaks the acyclicity of the existing graph.\n",
+ "3. **`causal_links`**: a set of causal-links. \n",
+ "A causal link between two actions _A_ and _B_ in the plan is written as _A_ --_p_--> _B_ and is read as \"A achieves p for B\".\n",
+ "This imples that _p_ is an effect of _A_ and a precondition of _B_.\n",
+ "It also asserts that _p_ must remain true from the time of action _A_ to the time of action _B_.\n",
+ "Any violation of this rule is called a threat and must be resolved immediately by adding suitable ordering constraints.\n",
+ "`causal_links` stores this information as tuples `(Action(A), precondition(p), Action(B))` which is interpreted as given above.\n",
+ "Causal-links can also be called **protection-intervals**, because the link _A_ --_p_--> _B_ protects _p_ from being negated over the interval from _A_ to _B_.\n",
+ "4. **`agenda`**: a set of open-preconditions.\n",
+ "A precondition is open if it is not achieved by some action in the plan.\n",
+ "Planners will work to reduce the set of open preconditions to the empty set, without introducing a contradiction.\n",
+ "`agenda` stored this information as tuples `(precondition(p), Action(A))` where p is a precondition of the action A.\n",
+ "\n",
+ "A **consistent plan** is a plan in which there are no cycles in the ordering constraints and no conflicts with the causal-links.\n",
+ "A consistent plan with no open preconditions is a **solution**.\n",
+ "
\n",
+ "
\n",
+ "Let's briefly glance over the helper functions before going into the actual algorithm.\n",
+ "
\n",
+ "**`expand_actions`**: generates all possible actions with variable bindings for use as a heuristic of selection of an open precondition.\n",
+ "
\n",
+ "**`find_open_precondition`**: finds a precondition from the agenda with the least number of actions that fulfil that precondition.\n",
+ "This heuristic helps form mandatory ordering constraints and causal-links to further simplify the problem and reduce the probability of encountering a threat.\n",
+ "
\n",
+ "**`find_action_for_precondition`**: finds an action that fulfils the given precondition along with the absolutely necessary variable bindings in accordance with the principle of _least commitment_.\n",
+ "In case of multiple possible actions, the action with the least number of effects is chosen to minimize the chances of encountering a threat.\n",
+ "
\n",
+ "**`cyclic`**: checks if a directed graph is cyclic.\n",
+ "
\n",
+ "**`add_const`**: adds `constraint` to `constraints` if the newly formed graph is acyclic and returns `constraints` otherwise.\n",
+ "
\n",
+ "**`is_a_threat`**: checks if the given `effect` negates the given `precondition`.\n",
+ "
\n",
+ "**`protect`**: checks if the given `action` poses a threat to the given `causal_link`.\n",
+ "If so, the threat is resolved by either promotion or demotion, whichever generates acyclic temporal constraints.\n",
+ "If neither promotion or demotion work, the chosen action is not the correct fit or the planning problem cannot be solved altogether.\n",
+ "
\n",
+ "**`convert`**: converts a graph from a list of edges to an `Action` : `set` mapping, for use in topological sorting.\n",
+ "
\n",
+ "**`toposort`**: a generator function that generates a topological ordering of a given graph as a list of sets.\n",
+ "Each set contains an action or several actions.\n",
+ "If a set has more that one action in it, it means that permutations between those actions also produce a valid plan.\n",
+ "
\n",
+ "**`display_plan`**: displays the `causal_links`, `constraints` and the partial order plan generated from `toposort`.\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The **`execute`** method executes the algorithm, which is summarized below:\n",
+ "
\n",
+ "1. An open precondition is selected (a sub-goal that we want to achieve).\n",
+ "2. An action that fulfils the open precondition is chosen.\n",
+ "3. Temporal constraints are updated.\n",
+ "4. Existing causal links are protected. Protection is a method that checks if the causal links conflict\n",
+ " and if they do, temporal constraints are added to fix the threats.\n",
+ "5. The set of open preconditions is updated.\n",
+ "6. Temporal constraints of the selected action and the next action are established.\n",
+ "7. A new causal link is added between the selected action and the owner of the open precondition.\n",
+ "8. The set of new causal links is checked for threats and if found, the threat is removed by either promotion or demotion.\n",
+ " If promotion or demotion is unable to solve the problem, the planning problem cannot be solved with the current sequence of actions\n",
+ " or it may not be solvable at all.\n",
+ "9. These steps are repeated until the set of open preconditions is empty."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "A partial-order plan can be used to generate different valid total-order plans.\n",
+ "This step is called **linearization** of the partial-order plan.\n",
+ "All possible linearizations of a partial-order plan for `socks_and_shoes` looks like this.\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "Linearization can be carried out in many ways, but the most efficient way is to represent the set of temporal constraints as a directed graph.\n",
+ "We can easily realize that the graph should also be acyclic as cycles in constraints means that the constraints are inconsistent.\n",
+ "This acyclicity is enforced by the `add_const` method, which adds a new constraint only if the acyclicity of the existing graph is not violated.\n",
+ "The `protect` method also checks for acyclicity of the newly-added temporal constraints to make a decision between promotion and demotion in case of a threat.\n",
+ "This property of a graph created from the temporal constraints of a valid partial-order plan allows us to use topological sort to order the constraints linearly.\n",
+ "A topological sort may produce several different valid solutions for a given directed acyclic graph."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now that we know how `PartialOrderPlanner` works, let's solve a few problems using it."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Causal Links\n",
+ "(Action(PutOn(Spare, Axle)), At(Spare, Axle), Action(Finish))\n",
+ "(Action(Start), Tire(Spare), Action(PutOn(Spare, Axle)))\n",
+ "(Action(Remove(Flat, Axle)), NotAt(Flat, Axle), Action(PutOn(Spare, Axle)))\n",
+ "(Action(Start), At(Flat, Axle), Action(Remove(Flat, Axle)))\n",
+ "(Action(Remove(Spare, Trunk)), At(Spare, Ground), Action(PutOn(Spare, Axle)))\n",
+ "(Action(Start), At(Spare, Trunk), Action(Remove(Spare, Trunk)))\n",
+ "(Action(Remove(Flat, Axle)), At(Flat, Ground), Action(Finish))\n",
+ "\n",
+ "Constraints\n",
+ "Action(Remove(Flat, Axle)) < Action(PutOn(Spare, Axle))\n",
+ "Action(Start) < Action(Finish)\n",
+ "Action(Remove(Spare, Trunk)) < Action(PutOn(Spare, Axle))\n",
+ "Action(Start) < Action(Remove(Spare, Trunk))\n",
+ "Action(Start) < Action(Remove(Flat, Axle))\n",
+ "Action(Remove(Flat, Axle)) < Action(Finish)\n",
+ "Action(PutOn(Spare, Axle)) < Action(Finish)\n",
+ "Action(Start) < Action(PutOn(Spare, Axle))\n",
+ "\n",
+ "Partial Order Plan\n",
+ "[{Action(Start)}, {Action(Remove(Flat, Axle)), Action(Remove(Spare, Trunk))}, {Action(PutOn(Spare, Axle))}, {Action(Finish)}]\n"
+ ]
+ }
+ ],
+ "source": [
+ "st = spare_tire()\n",
+ "pop = PartialOrderPlanner(st)\n",
+ "pop.execute()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We observe that in the given partial order plan, Remove(Flat, Axle) and Remove(Spare, Trunk) are in the same set.\n",
+ "This means that the order of performing these actions does not affect the final outcome.\n",
+ "That aside, we also see that the PutOn(Spare, Axle) action has to be performed after both the Remove actions are complete, which seems logically consistent."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Causal Links\n",
+ "(Action(FromTable(C, B)), On(C, B), Action(Finish))\n",
+ "(Action(FromTable(B, A)), On(B, A), Action(Finish))\n",
+ "(Action(Start), OnTable(B), Action(FromTable(B, A)))\n",
+ "(Action(Start), OnTable(C), Action(FromTable(C, B)))\n",
+ "(Action(Start), Clear(C), Action(FromTable(C, B)))\n",
+ "(Action(Start), Clear(A), Action(FromTable(B, A)))\n",
+ "(Action(ToTable(A, B)), Clear(B), Action(FromTable(C, B)))\n",
+ "(Action(Start), On(A, B), Action(ToTable(A, B)))\n",
+ "(Action(ToTable(A, B)), Clear(B), Action(FromTable(B, A)))\n",
+ "(Action(Start), Clear(A), Action(ToTable(A, B)))\n",
+ "\n",
+ "Constraints\n",
+ "Action(Start) < Action(FromTable(C, B))\n",
+ "Action(FromTable(B, A)) < Action(FromTable(C, B))\n",
+ "Action(Start) < Action(FromTable(B, A))\n",
+ "Action(Start) < Action(ToTable(A, B))\n",
+ "Action(Start) < Action(Finish)\n",
+ "Action(FromTable(B, A)) < Action(Finish)\n",
+ "Action(FromTable(C, B)) < Action(Finish)\n",
+ "Action(ToTable(A, B)) < Action(FromTable(B, A))\n",
+ "Action(ToTable(A, B)) < Action(FromTable(C, B))\n",
+ "\n",
+ "Partial Order Plan\n",
+ "[{Action(Start)}, {Action(ToTable(A, B))}, {Action(FromTable(B, A))}, {Action(FromTable(C, B))}, {Action(Finish)}]\n"
+ ]
+ }
+ ],
+ "source": [
+ "sbw = simple_blocks_world()\n",
+ "pop = PartialOrderPlanner(sbw)\n",
+ "pop.execute()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": true
+ },
+ "source": [
+ "We see that this plan does not have flexibility in selecting actions, ie, actions should be performed in this order and this order only, to successfully reach the goal state."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Causal Links\n",
+ "(Action(RightShoe), RightShoeOn, Action(Finish))\n",
+ "(Action(LeftShoe), LeftShoeOn, Action(Finish))\n",
+ "(Action(LeftSock), LeftSockOn, Action(LeftShoe))\n",
+ "(Action(RightSock), RightSockOn, Action(RightShoe))\n",
+ "\n",
+ "Constraints\n",
+ "Action(LeftSock) < Action(LeftShoe)\n",
+ "Action(RightSock) < Action(RightShoe)\n",
+ "Action(Start) < Action(RightShoe)\n",
+ "Action(Start) < Action(Finish)\n",
+ "Action(LeftShoe) < Action(Finish)\n",
+ "Action(Start) < Action(RightSock)\n",
+ "Action(Start) < Action(LeftShoe)\n",
+ "Action(Start) < Action(LeftSock)\n",
+ "Action(RightShoe) < Action(Finish)\n",
+ "\n",
+ "Partial Order Plan\n",
+ "[{Action(Start)}, {Action(LeftSock), Action(RightSock)}, {Action(LeftShoe), Action(RightShoe)}, {Action(Finish)}]\n"
+ ]
+ }
+ ],
+ "source": [
+ "ss = socks_and_shoes()\n",
+ "pop = PartialOrderPlanner(ss)\n",
+ "pop.execute()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "collapsed": true
+ },
+ "source": [
+ "This plan again doesn't have constraints in selecting socks or shoes.\n",
+ "As long as both socks are worn before both shoes, we are fine.\n",
+ "Notice however, there is one valid solution,\n",
+ "
\n",
+ "LeftSock -> LeftShoe -> RightSock -> RightShoe\n",
+ "
\n",
+ "that the algorithm could not find as it cannot be represented as a general partially-ordered plan but is a specific total-order solution."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Runtime differences\n",
+ "Let's briefly take a look at the running time of all the three algorithms on the `socks_and_shoes` problem."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "ss = socks_and_shoes()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "198 µs ± 3.53 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%timeit\n",
+ "GraphPlan(ss).execute()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "844 µs ± 23.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%timeit\n",
+ "Linearize(ss).execute()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "258 µs ± 4.03 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%timeit\n",
+ "PartialOrderPlanner(ss).execute(display=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We observe that `GraphPlan` is about 4 times faster than `Linearize` because `Linearize` essentially runs a `GraphPlan` subroutine under the hood and then carries out some transformations on the solved planning-graph.\n",
+ "
\n",
+ "We also find that `GraphPlan` is slightly faster than `PartialOrderPlanner`, but this is mainly due to the `expand_actions` method in `PartialOrderPlanner` that slows it down as it generates all possible permutations of actions and variable bindings.\n",
+ "
\n",
+ "Without heuristic functions, `PartialOrderPlanner` will be atleast as fast as `GraphPlan`, if not faster, but will have a higher tendency to encounter threats and conflicts which might take additional time to resolve.\n",
+ "
\n",
+ "Different planning algorithms work differently for different problems."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.5.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/planning_total_order_planner.ipynb b/planning_total_order_planner.ipynb
new file mode 100644
index 000000000..b94941ece
--- /dev/null
+++ b/planning_total_order_planner.ipynb
@@ -0,0 +1,341 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### TOTAL ORDER PLANNER\n",
+ "\n",
+ "In mathematical terminology, **total order**, **linear order** or **simple order** refers to a set *X* which is said to be totally ordered under ≤ if the following statements hold for all *a*, *b* and *c* in *X*:\n",
+ "
\n",
+ "If *a* ≤ *b* and *b* ≤ *a*, then *a* = *b* (antisymmetry).\n",
+ "
\n",
+ "If *a* ≤ *b* and *b* ≤ *c*, then *a* ≤ *c* (transitivity).\n",
+ "
\n",
+ "*a* ≤ *b* or *b* ≤ *a* (connex relation).\n",
+ "\n",
+ "
\n",
+ "In simpler terms, a total order plan is a linear ordering of actions to be taken to reach the goal state.\n",
+ "There may be several different total-order plans for a particular goal depending on the problem.\n",
+ "
\n",
+ "
\n",
+ "In the module, the `Linearize` class solves problems using this paradigm.\n",
+ "At its core, the `Linearize` uses a solved planning graph from `GraphPlan` and finds a valid total-order solution for it.\n",
+ "Let's have a look at the class."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from planning import *\n",
+ "from notebook import psource"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "class Linearize:\n",
+ "\n",
+ " def __init__(self, planningproblem):\n",
+ " self.planningproblem = planningproblem\n",
+ "\n",
+ " def filter(self, solution):\n",
+ " """Filter out persistence actions from a solution"""\n",
+ "\n",
+ " new_solution = []\n",
+ " for section in solution[0]:\n",
+ " new_section = []\n",
+ " for operation in section:\n",
+ " if not (operation.op[0] == 'P' and operation.op[1].isupper()):\n",
+ " new_section.append(operation)\n",
+ " new_solution.append(new_section)\n",
+ " return new_solution\n",
+ "\n",
+ " def orderlevel(self, level, planningproblem):\n",
+ " """Return valid linear order of actions for a given level"""\n",
+ "\n",
+ " for permutation in itertools.permutations(level):\n",
+ " temp = copy.deepcopy(planningproblem)\n",
+ " count = 0\n",
+ " for action in permutation:\n",
+ " try:\n",
+ " temp.act(action)\n",
+ " count += 1\n",
+ " except:\n",
+ " count = 0\n",
+ " temp = copy.deepcopy(planningproblem)\n",
+ " break\n",
+ " if count == len(permutation):\n",
+ " return list(permutation), temp\n",
+ " return None\n",
+ "\n",
+ " def execute(self):\n",
+ " """Finds total-order solution for a planning graph"""\n",
+ "\n",
+ " graphplan_solution = GraphPlan(self.planningproblem).execute()\n",
+ " filtered_solution = self.filter(graphplan_solution)\n",
+ " ordered_solution = []\n",
+ " planningproblem = self.planningproblem\n",
+ " for level in filtered_solution:\n",
+ " level_solution, planningproblem = self.orderlevel(level, planningproblem)\n",
+ " for element in level_solution:\n",
+ " ordered_solution.append(element)\n",
+ "\n",
+ " return ordered_solution\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(Linearize)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The `filter` method removes the persistence actions (if any) from the planning graph representation.\n",
+ "
\n",
+ "The `orderlevel` method finds a valid total-ordering of a specified level of the planning-graph, given the state of the graph after the previous level.\n",
+ "
\n",
+ "The `execute` method sequentially calls `orderlevel` for all the levels in the planning-graph and returns the final total-order solution.\n",
+ "
\n",
+ "
\n",
+ "Let's look at some examples."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[Load(C1, P1, SFO),\n",
+ " Fly(P1, SFO, JFK),\n",
+ " Load(C2, P2, JFK),\n",
+ " Fly(P2, JFK, SFO),\n",
+ " Unload(C2, P2, SFO),\n",
+ " Unload(C1, P1, JFK)]"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# total-order solution for air_cargo problem\n",
+ "Linearize(air_cargo()).execute()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[Remove(Spare, Trunk), Remove(Flat, Axle), PutOn(Spare, Axle)]"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# total-order solution for spare_tire problem\n",
+ "Linearize(spare_tire()).execute()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[MoveToTable(C, A), Move(B, Table, C), Move(A, Table, B)]"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# total-order solution for three_block_tower problem\n",
+ "Linearize(three_block_tower()).execute()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[ToTable(A, B), FromTable(B, A), FromTable(C, B)]"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# total-order solution for simple_blocks_world problem\n",
+ "Linearize(simple_blocks_world()).execute()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[RightSock, LeftSock, RightShoe, LeftShoe]"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# total-order solution for socks_and_shoes problem\n",
+ "Linearize(socks_and_shoes()).execute()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.5.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/probabilistic_learning.py b/probabilistic_learning.py
new file mode 100644
index 000000000..1138e702d
--- /dev/null
+++ b/probabilistic_learning.py
@@ -0,0 +1,154 @@
+"""Learning probabilistic models. (Chapters 20)"""
+
+import heapq
+
+from utils import weighted_sampler, product, gaussian
+
+
+class CountingProbDist:
+ """
+ A probability distribution formed by observing and counting examples.
+ If p is an instance of this class and o is an observed value, then
+ there are 3 main operations:
+ p.add(o) increments the count for observation o by 1.
+ p.sample() returns a random element from the distribution.
+ p[o] returns the probability for o (as in a regular ProbDist).
+ """
+
+ def __init__(self, observations=None, default=0):
+ """
+ Create a distribution, and optionally add in some observations.
+ By default this is an unsmoothed distribution, but saying default=1,
+ for example, gives you add-one smoothing.
+ """
+ if observations is None:
+ observations = []
+ self.dictionary = {}
+ self.n_obs = 0
+ self.default = default
+ self.sampler = None
+
+ for o in observations:
+ self.add(o)
+
+ def add(self, o):
+ """Add an observation o to the distribution."""
+ self.smooth_for(o)
+ self.dictionary[o] += 1
+ self.n_obs += 1
+ self.sampler = None
+
+ def smooth_for(self, o):
+ """
+ Include o among the possible observations, whether or not
+ it's been observed yet.
+ """
+ if o not in self.dictionary:
+ self.dictionary[o] = self.default
+ self.n_obs += self.default
+ self.sampler = None
+
+ def __getitem__(self, item):
+ """Return an estimate of the probability of item."""
+ self.smooth_for(item)
+ return self.dictionary[item] / self.n_obs
+
+ # (top() and sample() are not used in this module, but elsewhere.)
+
+ def top(self, n):
+ """Return (count, obs) tuples for the n most frequent observations."""
+ return heapq.nlargest(n, [(v, k) for (k, v) in self.dictionary.items()])
+
+ def sample(self):
+ """Return a random sample from the distribution."""
+ if self.sampler is None:
+ self.sampler = weighted_sampler(list(self.dictionary.keys()), list(self.dictionary.values()))
+ return self.sampler()
+
+
+def NaiveBayesLearner(dataset, continuous=True, simple=False):
+ if simple:
+ return NaiveBayesSimple(dataset)
+ if continuous:
+ return NaiveBayesContinuous(dataset)
+ else:
+ return NaiveBayesDiscrete(dataset)
+
+
+def NaiveBayesSimple(distribution):
+ """
+ A simple naive bayes classifier that takes as input a dictionary of
+ CountingProbDist objects and classifies items according to these distributions.
+ The input dictionary is in the following form:
+ (ClassName, ClassProb): CountingProbDist
+ """
+ target_dist = {c_name: prob for c_name, prob in distribution.keys()}
+ attr_dists = {c_name: count_prob for (c_name, _), count_prob in distribution.items()}
+
+ def predict(example):
+ """Predict the target value for example. Calculate probabilities for each
+ class and pick the max."""
+
+ def class_probability(target_val):
+ attr_dist = attr_dists[target_val]
+ return target_dist[target_val] * product(attr_dist[a] for a in example)
+
+ return max(target_dist.keys(), key=class_probability)
+
+ return predict
+
+
+def NaiveBayesDiscrete(dataset):
+ """
+ Just count how many times each value of each input attribute
+ occurs, conditional on the target value. Count the different
+ target values too.
+ """
+
+ target_vals = dataset.values[dataset.target]
+ target_dist = CountingProbDist(target_vals)
+ attr_dists = {(gv, attr): CountingProbDist(dataset.values[attr]) for gv in target_vals for attr in dataset.inputs}
+ for example in dataset.examples:
+ target_val = example[dataset.target]
+ target_dist.add(target_val)
+ for attr in dataset.inputs:
+ attr_dists[target_val, attr].add(example[attr])
+
+ def predict(example):
+ """
+ Predict the target value for example. Consider each possible value,
+ and pick the most likely by looking at each attribute independently.
+ """
+
+ def class_probability(target_val):
+ return (target_dist[target_val] * product(attr_dists[target_val, attr][example[attr]]
+ for attr in dataset.inputs))
+
+ return max(target_vals, key=class_probability)
+
+ return predict
+
+
+def NaiveBayesContinuous(dataset):
+ """
+ Count how many times each target value occurs.
+ Also, find the means and deviations of input attribute values for each target value.
+ """
+ means, deviations = dataset.find_means_and_deviations()
+
+ target_vals = dataset.values[dataset.target]
+ target_dist = CountingProbDist(target_vals)
+
+ def predict(example):
+ """Predict the target value for example. Consider each possible value,
+ and pick the most likely by looking at each attribute independently."""
+
+ def class_probability(target_val):
+ prob = target_dist[target_val]
+ for attr in dataset.inputs:
+ prob *= gaussian(means[target_val][attr], deviations[target_val][attr], example[attr])
+ return prob
+
+ return max(target_vals, key=class_probability)
+
+ return predict
diff --git a/probability.ipynb b/probability.ipynb
index 7b1cd3605..fe9643a83 100644
--- a/probability.ipynb
+++ b/probability.ipynb
@@ -2,55 +2,243 @@
"cells": [
{
"cell_type": "markdown",
- "metadata": {
- "collapsed": false
- },
+ "metadata": {},
"source": [
"# Probability \n",
"\n",
- "This IPy notebook acts as supporting material for **Chapter 13 Quantifying Uncertainty**, **Chapter 14 Probabilistic Reasoning** and **Chapter 15 Probabilistic Reasoning over Time** of the book* Artificial Intelligence: A Modern Approach*. This notebook makes use of the implementations in probability.py module. Let us import everything from the probability module. It might be helpful to view the source of some of our implementations. Please refer to the Introductory IPy file for more details on how to do so."
+ "This IPy notebook acts as supporting material for topics covered in **Chapter 13 Quantifying Uncertainty**, **Chapter 14 Probabilistic Reasoning**, **Chapter 15 Probabilistic Reasoning over Time**, **Chapter 16 Making Simple Decisions** and parts of **Chapter 25 Robotics** of the book* Artificial Intelligence: A Modern Approach*. This notebook makes use of the implementations in probability.py module. Let us import everything from the probability module. It might be helpful to view the source of some of our implementations. Please refer to the Introductory IPy file for more details on how to do so."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "execution_count": 1,
+ "metadata": {},
"outputs": [],
"source": [
- "from probability import *"
+ "from probability import *\n",
+ "from utils import print_table\n",
+ "from notebook import psource, pseudocode, heatmap"
]
},
{
"cell_type": "markdown",
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
+ "source": [
+ "## CONTENTS\n",
+ "- Probability Distribution\n",
+ " - Joint probability distribution\n",
+ " - Inference using full joint distributions\n",
+ "
\n",
+ "- Bayesian Networks\n",
+ " - BayesNode\n",
+ " - BayesNet\n",
+ " - Exact Inference in Bayesian Networks\n",
+ " - Enumeration\n",
+ " - Variable elimination\n",
+ " - Approximate Inference in Bayesian Networks\n",
+ " - Prior sample\n",
+ " - Rejection sampling\n",
+ " - Likelihood weighting\n",
+ " - Gibbs sampling\n",
+ "
\n",
+ "- Hidden Markov Models\n",
+ " - Inference in Hidden Markov Models\n",
+ " - Forward-backward\n",
+ " - Fixed lag smoothing\n",
+ " - Particle filtering\n",
+ "
\n",
+ "
\n",
+ "- Monte Carlo Localization\n",
+ "- Decision Theoretic Agent\n",
+ "- Information Gathering Agent"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
"source": [
- "## Probability Distribution\n",
+ "## PROBABILITY DISTRIBUTION\n",
"\n",
"Let us begin by specifying discrete probability distributions. The class **ProbDist** defines a discrete probability distribution. We name our random variable and then assign probabilities to the different values of the random variable. Assigning probabilities to the values works similar to that of using a dictionary with keys being the Value and we assign to it the probability. This is possible because of the magic methods **_ _getitem_ _** and **_ _setitem_ _** which store the probabilities in the prob dict of the object. You can keep the source window open alongside while playing with the rest of the code to get a better understanding."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "class ProbDist:\n",
+ " """A discrete probability distribution. You name the random variable\n",
+ " in the constructor, then assign and query probability of values.\n",
+ " >>> P = ProbDist('Flip'); P['H'], P['T'] = 0.25, 0.75; P['H']\n",
+ " 0.25\n",
+ " >>> P = ProbDist('X', {'lo': 125, 'med': 375, 'hi': 500})\n",
+ " >>> P['lo'], P['med'], P['hi']\n",
+ " (0.125, 0.375, 0.5)\n",
+ " """\n",
+ "\n",
+ " def __init__(self, varname='?', freqs=None):\n",
+ " """If freqs is given, it is a dictionary of values - frequency pairs,\n",
+ " then ProbDist is normalized."""\n",
+ " self.prob = {}\n",
+ " self.varname = varname\n",
+ " self.values = []\n",
+ " if freqs:\n",
+ " for (v, p) in freqs.items():\n",
+ " self[v] = p\n",
+ " self.normalize()\n",
+ "\n",
+ " def __getitem__(self, val):\n",
+ " """Given a value, return P(value)."""\n",
+ " try:\n",
+ " return self.prob[val]\n",
+ " except KeyError:\n",
+ " return 0\n",
+ "\n",
+ " def __setitem__(self, val, p):\n",
+ " """Set P(val) = p."""\n",
+ " if val not in self.values:\n",
+ " self.values.append(val)\n",
+ " self.prob[val] = p\n",
+ "\n",
+ " def normalize(self):\n",
+ " """Make sure the probabilities of all values sum to 1.\n",
+ " Returns the normalized distribution.\n",
+ " Raises a ZeroDivisionError if the sum of the values is 0."""\n",
+ " total = sum(self.prob.values())\n",
+ " if not isclose(total, 1.0):\n",
+ " for val in self.prob:\n",
+ " self.prob[val] /= total\n",
+ " return self\n",
+ "\n",
+ " def show_approx(self, numfmt='{:.3g}'):\n",
+ " """Show the probabilities rounded and sorted by key, for the\n",
+ " sake of portable doctests."""\n",
+ " return ', '.join([('{}: ' + numfmt).format(v, p)\n",
+ " for (v, p) in sorted(self.prob.items())])\n",
+ "\n",
+ " def __repr__(self):\n",
+ " return "P({})".format(self.varname)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource ProbDist"
+ "psource(ProbDist)"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0.75"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"p = ProbDist('Flip')\n",
"p['H'], p['T'] = 0.25, 0.75\n",
@@ -61,28 +249,46 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "The first parameter of the constructor **varname** has a default value of '?'. So if the name is not passed it defaults to ?. The keyword argument **freqs** can be a dictionary of values of random variable:probability. These are then normalized such that the probability values sum upto 1 using the **normalize** method."
+ "The first parameter of the constructor **varname** has a default value of '?'. So if the name is not passed it defaults to ?. The keyword argument **freqs** can be a dictionary of values of random variable: probability. These are then normalized such that the probability values sum upto 1 using the **normalize** method."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'?'"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"p = ProbDist(freqs={'low': 125, 'medium': 375, 'high': 500})\n",
- "p.varname\n"
+ "p.varname"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(0.125, 0.375, 0.5)"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"(p['low'], p['medium'], p['high'])"
]
@@ -96,11 +302,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['low', 'medium', 'high']"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"p.values"
]
@@ -109,16 +324,25 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "The distribution by default is not normalized if values are added incremently. We can still force normalization by invoking the **normalize** method."
+ "The distribution by default is not normalized if values are added incrementally. We can still force normalization by invoking the **normalize** method."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(50, 114, 64)"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"p = ProbDist('Y')\n",
"p['Cat'] = 50\n",
@@ -129,11 +353,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(0.21929824561403508, 0.5, 0.2807017543859649)"
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"p.normalize()\n",
"(p['Cat'], p['Dog'], p['Mice'])"
@@ -148,11 +381,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'Cat: 0.219, Dog: 0.5, Mice: 0.281'"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"p.show_approx()"
]
@@ -171,35 +413,175 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(8, 10)"
+ ]
+ },
+ "execution_count": 10,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"event = {'A': 10, 'B': 9, 'C': 8}\n",
"variables = ['C', 'A']\n",
- "event_values (event, variables)"
+ "event_values(event, variables)"
]
},
{
"cell_type": "markdown",
- "metadata": {
- "collapsed": true
- },
+ "metadata": {},
"source": [
"_A probability model is completely determined by the joint distribution for all of the random variables._ (**Section 13.3**) The probability module implements these as the class **JointProbDist** which inherits from the **ProbDist** class. This class specifies a discrete probability distribute over a set of variables. "
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "class JointProbDist(ProbDist):\n",
+ " """A discrete probability distribute over a set of variables.\n",
+ " >>> P = JointProbDist(['X', 'Y']); P[1, 1] = 0.25\n",
+ " >>> P[1, 1]\n",
+ " 0.25\n",
+ " >>> P[dict(X=0, Y=1)] = 0.5\n",
+ " >>> P[dict(X=0, Y=1)]\n",
+ " 0.5"""\n",
+ "\n",
+ " def __init__(self, variables):\n",
+ " self.prob = {}\n",
+ " self.variables = variables\n",
+ " self.vals = defaultdict(list)\n",
+ "\n",
+ " def __getitem__(self, values):\n",
+ " """Given a tuple or dict of values, return P(values)."""\n",
+ " values = event_values(values, self.variables)\n",
+ " return ProbDist.__getitem__(self, values)\n",
+ "\n",
+ " def __setitem__(self, values, p):\n",
+ " """Set P(values) = p. Values can be a tuple or a dict; it must\n",
+ " have a value for each of the variables in the joint. Also keep track\n",
+ " of the values we have seen so far for each variable."""\n",
+ " values = event_values(values, self.variables)\n",
+ " self.prob[values] = p\n",
+ " for var, val in zip(self.variables, values):\n",
+ " if val not in self.vals[var]:\n",
+ " self.vals[var].append(val)\n",
+ "\n",
+ " def values(self, var):\n",
+ " """Return the set of possible values for a variable."""\n",
+ " return self.vals[var]\n",
+ "\n",
+ " def __repr__(self):\n",
+ " return "P({})".format(self.variables)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource JointProbDist"
+ "psource(JointProbDist)"
]
},
{
@@ -213,11 +595,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "P(['X', 'Y'])"
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"variables = ['X', 'Y']\n",
"j = JointProbDist(variables)\n",
@@ -234,11 +625,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(0.2, 0.5)"
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"j[1,1] = 0.2\n",
"j[dict(X=0, Y=1)] = 0.5\n",
@@ -255,11 +655,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[1, 0]"
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"j.values('X')"
]
@@ -274,7 +683,7 @@
"\n",
"This is illustrated in **Section 13.3** of the book. The functions **enumerate_joint** and **enumerate_joint_ask** implement this functionality. Under the hood they implement **Equation 13.9** from the book.\n",
"\n",
- "$$\\textbf{P}(X | \\textbf{e}) = α \\textbf{P}(X, \\textbf{e}) = α \\sum_{y} \\textbf{P}(X, \\textbf{e}, \\textbf{y})$$\n",
+ "$$\\textbf{P}(X | \\textbf{e}) = \\alpha \\textbf{P}(X, \\textbf{e}) = \\alpha \\sum_{y} \\textbf{P}(X, \\textbf{e}, \\textbf{y})$$\n",
"\n",
"Here **α** is the normalizing factor. **X** is our query variable and **e** is the evidence. According to the equation we enumerate on the remaining variables **y** (not in evidence or query variable) i.e. all possible combinations of **y**\n",
"\n",
@@ -283,10 +692,8 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
+ "execution_count": 15,
+ "metadata": {},
"outputs": [],
"source": [
"full_joint = JointProbDist(['Cavity', 'Toothache', 'Catch'])\n",
@@ -309,13 +716,119 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def enumerate_joint(variables, e, P):\n",
+ " """Return the sum of those entries in P consistent with e,\n",
+ " provided variables is P's remaining variables (the ones not in e)."""\n",
+ " if not variables:\n",
+ " return P[e]\n",
+ " Y, rest = variables[0], variables[1:]\n",
+ " return sum([enumerate_joint(rest, extend(e, Y, y), P)\n",
+ " for y in P.values(Y)])\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource enumerate_joint"
+ "psource(enumerate_joint)"
]
},
{
@@ -327,11 +840,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0.19999999999999998"
+ ]
+ },
+ "execution_count": 17,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"evidence = dict(Toothache=True)\n",
"variables = ['Cavity', 'Catch'] # variables not part of evidence\n",
@@ -348,11 +870,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0.12"
+ ]
+ },
+ "execution_count": 18,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"evidence = dict(Cavity=True, Toothache=True)\n",
"variables = ['Catch'] # variables not part of evidence\n",
@@ -371,11 +902,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 19,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0.6"
+ ]
+ },
+ "execution_count": 19,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"ans2/ans1"
]
@@ -389,13 +929,125 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def enumerate_joint_ask(X, e, P):\n",
+ " """Return a probability distribution over the values of the variable X,\n",
+ " given the {var:val} observations e, in the JointProbDist P. [Section 13.3]\n",
+ " >>> P = JointProbDist(['X', 'Y'])\n",
+ " >>> P[0,0] = 0.25; P[0,1] = 0.5; P[1,1] = P[2,1] = 0.125\n",
+ " >>> enumerate_joint_ask('X', dict(Y=1), P).show_approx()\n",
+ " '0: 0.667, 1: 0.167, 2: 0.167'\n",
+ " """\n",
+ " assert X not in e, "Query variable must be distinct from evidence"\n",
+ " Q = ProbDist(X) # probability distribution for X, initially empty\n",
+ " Y = [v for v in P.variables if v != X and v not in e] # hidden variables.\n",
+ " for xi in P.values(X):\n",
+ " Q[xi] = enumerate_joint(Y, extend(e, X, xi), P)\n",
+ " return Q.normalize()\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource enumerate_joint_ask"
+ "psource(enumerate_joint_ask)"
]
},
{
@@ -407,11 +1059,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(0.6, 0.39999999999999997)"
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"query_variable = 'Cavity'\n",
"evidence = dict(Toothache=True)\n",
@@ -430,7 +1091,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Bayesian Networks\n",
+ "## BAYESIAN NETWORKS\n",
"\n",
"A Bayesian network is a representation of the joint probability distribution encoding a collection of conditional independence statements.\n",
"\n",
@@ -441,13 +1102,182 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "class BayesNode:\n",
+ " """A conditional probability distribution for a boolean variable,\n",
+ " P(X | parents). Part of a BayesNet."""\n",
+ "\n",
+ " def __init__(self, X, parents, cpt):\n",
+ " """X is a variable name, and parents a sequence of variable\n",
+ " names or a space-separated string. cpt, the conditional\n",
+ " probability table, takes one of these forms:\n",
+ "\n",
+ " * A number, the unconditional probability P(X=true). You can\n",
+ " use this form when there are no parents.\n",
+ "\n",
+ " * A dict {v: p, ...}, the conditional probability distribution\n",
+ " P(X=true | parent=v) = p. When there's just one parent.\n",
+ "\n",
+ " * A dict {(v1, v2, ...): p, ...}, the distribution P(X=true |\n",
+ " parent1=v1, parent2=v2, ...) = p. Each key must have as many\n",
+ " values as there are parents. You can use this form always;\n",
+ " the first two are just conveniences.\n",
+ "\n",
+ " In all cases the probability of X being false is left implicit,\n",
+ " since it follows from P(X=true).\n",
+ "\n",
+ " >>> X = BayesNode('X', '', 0.2)\n",
+ " >>> Y = BayesNode('Y', 'P', {T: 0.2, F: 0.7})\n",
+ " >>> Z = BayesNode('Z', 'P Q',\n",
+ " ... {(T, T): 0.2, (T, F): 0.3, (F, T): 0.5, (F, F): 0.7})\n",
+ " """\n",
+ " if isinstance(parents, str):\n",
+ " parents = parents.split()\n",
+ "\n",
+ " # We store the table always in the third form above.\n",
+ " if isinstance(cpt, (float, int)): # no parents, 0-tuple\n",
+ " cpt = {(): cpt}\n",
+ " elif isinstance(cpt, dict):\n",
+ " # one parent, 1-tuple\n",
+ " if cpt and isinstance(list(cpt.keys())[0], bool):\n",
+ " cpt = {(v,): p for v, p in cpt.items()}\n",
+ "\n",
+ " assert isinstance(cpt, dict)\n",
+ " for vs, p in cpt.items():\n",
+ " assert isinstance(vs, tuple) and len(vs) == len(parents)\n",
+ " assert all(isinstance(v, bool) for v in vs)\n",
+ " assert 0 <= p <= 1\n",
+ "\n",
+ " self.variable = X\n",
+ " self.parents = parents\n",
+ " self.cpt = cpt\n",
+ " self.children = []\n",
+ "\n",
+ " def p(self, value, event):\n",
+ " """Return the conditional probability\n",
+ " P(X=value | parents=parent_values), where parent_values\n",
+ " are the values of parents in event. (event must assign each\n",
+ " parent a value.)\n",
+ " >>> bn = BayesNode('X', 'Burglary', {T: 0.2, F: 0.625})\n",
+ " >>> bn.p(False, {'Burglary': False, 'Earthquake': True})\n",
+ " 0.375"""\n",
+ " assert isinstance(value, bool)\n",
+ " ptrue = self.cpt[event_values(event, self.parents)]\n",
+ " return ptrue if value else 1 - ptrue\n",
+ "\n",
+ " def sample(self, event):\n",
+ " """Sample from the distribution for this variable conditioned\n",
+ " on event's values for parent_variables. That is, return True/False\n",
+ " at random according with the conditional probability given the\n",
+ " parents."""\n",
+ " return probability(self.p(True, event))\n",
+ "\n",
+ " def __repr__(self):\n",
+ " return repr((self.variable, ' '.join(self.parents)))\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource BayesNode"
+ "psource(BayesNode)"
]
},
{
@@ -465,10 +1295,8 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "execution_count": 23,
+ "metadata": {},
"outputs": [],
"source": [
"alarm_node = BayesNode('Alarm', ['Burglary', 'Earthquake'], \n",
@@ -484,15 +1312,13 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "execution_count": 24,
+ "metadata": {},
"outputs": [],
"source": [
"john_node = BayesNode('JohnCalls', ['Alarm'], {True: 0.90, False: 0.05})\n",
"mary_node = BayesNode('MaryCalls', 'Alarm', {(True, ): 0.70, (False, ): 0.01}) # Using string for parents.\n",
- "# Equvivalant to john_node definition. "
+ "# Equivalant to john_node definition."
]
},
{
@@ -504,10 +1330,8 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "execution_count": 25,
+ "metadata": {},
"outputs": [],
"source": [
"burglary_node = BayesNode('Burglary', '', 0.001)\n",
@@ -523,11 +1347,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 26,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0.09999999999999998"
+ ]
+ },
+ "execution_count": 26,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"john_node.p(False, {'Alarm': True, 'Burglary': True}) # P(JohnCalls=False | Alarm=True)"
]
@@ -541,13 +1374,148 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 27,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "class BayesNet:\n",
+ " """Bayesian network containing only boolean-variable nodes."""\n",
+ "\n",
+ " def __init__(self, node_specs=None):\n",
+ " """Nodes must be ordered with parents before children."""\n",
+ " self.nodes = []\n",
+ " self.variables = []\n",
+ " node_specs = node_specs or []\n",
+ " for node_spec in node_specs:\n",
+ " self.add(node_spec)\n",
+ "\n",
+ " def add(self, node_spec):\n",
+ " """Add a node to the net. Its parents must already be in the\n",
+ " net, and its variable must not."""\n",
+ " node = BayesNode(*node_spec)\n",
+ " assert node.variable not in self.variables\n",
+ " assert all((parent in self.variables) for parent in node.parents)\n",
+ " self.nodes.append(node)\n",
+ " self.variables.append(node.variable)\n",
+ " for parent in node.parents:\n",
+ " self.variable_node(parent).children.append(node)\n",
+ "\n",
+ " def variable_node(self, var):\n",
+ " """Return the node for the variable named var.\n",
+ " >>> burglary.variable_node('Burglary').variable\n",
+ " 'Burglary'"""\n",
+ " for n in self.nodes:\n",
+ " if n.variable == var:\n",
+ " return n\n",
+ " raise Exception("No such variable: {}".format(var))\n",
+ "\n",
+ " def variable_values(self, var):\n",
+ " """Return the domain of var."""\n",
+ " return [True, False]\n",
+ "\n",
+ " def __repr__(self):\n",
+ " return 'BayesNet({0!r})'.format(self.nodes)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource BayesNet"
+ "psource(BayesNet)"
]
},
{
@@ -572,11 +1540,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 28,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "BayesNet([('Burglary', ''), ('Earthquake', ''), ('Alarm', 'Burglary Earthquake'), ('JohnCalls', 'Alarm'), ('MaryCalls', 'Alarm')])"
+ ]
+ },
+ "execution_count": 28,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"burglary"
]
@@ -590,22 +1567,43 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 29,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "probability.BayesNode"
+ ]
+ },
+ "execution_count": 29,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"type(burglary.variable_node('Alarm'))"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 30,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{(True, True): 0.95,\n",
+ " (True, False): 0.94,\n",
+ " (False, True): 0.29,\n",
+ " (False, False): 0.001}"
+ ]
+ },
+ "execution_count": 30,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"burglary.variable_node('Alarm').cpt"
]
@@ -627,20 +1625,132 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 31,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def enumerate_all(variables, e, bn):\n",
+ " """Return the sum of those entries in P(variables | e{others})\n",
+ " consistent with e, where P is the joint distribution represented\n",
+ " by bn, and e{others} means e restricted to bn's other variables\n",
+ " (the ones other than variables). Parents must precede children in variables."""\n",
+ " if not variables:\n",
+ " return 1.0\n",
+ " Y, rest = variables[0], variables[1:]\n",
+ " Ynode = bn.variable_node(Y)\n",
+ " if Y in e:\n",
+ " return Ynode.p(e[Y], e) * enumerate_all(rest, e, bn)\n",
+ " else:\n",
+ " return sum(Ynode.p(y, e) * enumerate_all(rest, extend(e, Y, y), bn)\n",
+ " for y in bn.variable_values(Y))\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource enumerate_all"
+ "psource(enumerate_all)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "**enumerate__all** recursively evaluates a general form of the **Equation 14.4** in the book.\n",
+ "**enumerate_all** recursively evaluates a general form of the **Equation 14.4** in the book.\n",
"\n",
"$$\\textbf{P}(X | \\textbf{e}) = α \\textbf{P}(X, \\textbf{e}) = α \\sum_{y} \\textbf{P}(X, \\textbf{e}, \\textbf{y})$$ \n",
"\n",
@@ -651,29 +1761,147 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 32,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def enumeration_ask(X, e, bn):\n",
+ " """Return the conditional probability distribution of variable X\n",
+ " given evidence e, from BayesNet bn. [Figure 14.9]\n",
+ " >>> enumeration_ask('Burglary', dict(JohnCalls=T, MaryCalls=T), burglary\n",
+ " ... ).show_approx()\n",
+ " 'False: 0.716, True: 0.284'"""\n",
+ " assert X not in e, "Query variable must be distinct from evidence"\n",
+ " Q = ProbDist(X)\n",
+ " for xi in bn.variable_values(X):\n",
+ " Q[xi] = enumerate_all(bn.variables, extend(e, X, xi), bn)\n",
+ " return Q.normalize()\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource enumeration_ask"
+ "psource(enumeration_ask)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Let us solve the problem of finding out **P(Burglary=True | JohnCalls=True, MaryCalls=True)** using the **burglary** network.**enumeration_ask** takes three arguments **X** = variable name, **e** = Evidence (in form a dict like previously explained), **bn** = The Bayes Net to do inference on."
+ "Let us solve the problem of finding out **P(Burglary=True | JohnCalls=True, MaryCalls=True)** using the **burglary** network. **enumeration_ask** takes three arguments **X** = variable name, **e** = Evidence (in form a dict like previously explained), **bn** = The Bayes Net to do inference on."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 33,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0.2841718353643929"
+ ]
+ },
+ "execution_count": 33,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"ans_dist = enumeration_ask('Burglary', {'JohnCalls': True, 'MaryCalls': True}, burglary)\n",
"ans_dist[True]"
@@ -699,13 +1927,120 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 34,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def make_factor(var, e, bn):\n",
+ " """Return the factor for var in bn's joint distribution given e.\n",
+ " That is, bn's full joint distribution, projected to accord with e,\n",
+ " is the pointwise product of these factors for bn's variables."""\n",
+ " node = bn.variable_node(var)\n",
+ " variables = [X for X in [var] + node.parents if X not in e]\n",
+ " cpt = {event_values(e1, variables): node.p(e1[var], e1)\n",
+ " for e1 in all_events(variables, bn, e)}\n",
+ " return Factor(variables, cpt)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource make_factor"
+ "psource(make_factor)"
]
},
{
@@ -721,13 +2056,120 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 35,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def all_events(variables, bn, e):\n",
+ " """Yield every way of extending e with values for all variables."""\n",
+ " if not variables:\n",
+ " yield e\n",
+ " else:\n",
+ " X, rest = variables[0], variables[1:]\n",
+ " for e1 in all_events(rest, bn, e):\n",
+ " for x in bn.variable_values(X):\n",
+ " yield extend(e1, X, x)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource all_events"
+ "psource(all_events)"
]
},
{
@@ -741,10 +2183,8 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
+ "execution_count": 36,
+ "metadata": {},
"outputs": [],
"source": [
"f5 = make_factor('MaryCalls', {'JohnCalls': True, 'MaryCalls': True}, burglary)"
@@ -752,33 +2192,60 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 37,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 37,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"f5"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 38,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{(True,): 0.7, (False,): 0.01}"
+ ]
+ },
+ "execution_count": 38,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"f5.cpt"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 39,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "['Alarm']"
+ ]
+ },
+ "execution_count": 39,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"f5.variables"
]
@@ -792,10 +2259,8 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "execution_count": 40,
+ "metadata": {},
"outputs": [],
"source": [
"new_factor = make_factor('MaryCalls', {'Alarm': True}, burglary)"
@@ -803,11 +2268,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 41,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{(True,): 0.7, (False,): 0.30000000000000004}"
+ ]
+ },
+ "execution_count": 41,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"new_factor.cpt"
]
@@ -825,13 +2299,117 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 42,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " def pointwise_product(self, other, bn):\n",
+ " """Multiply two factors, combining their variables."""\n",
+ " variables = list(set(self.variables) | set(other.variables))\n",
+ " cpt = {event_values(e, variables): self.p(e) * other.p(e)\n",
+ " for e in all_events(variables, bn, {})}\n",
+ " return Factor(variables, cpt)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource Factor.pointwise_product"
+ "psource(Factor.pointwise_product)"
]
},
{
@@ -843,13 +2421,113 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 43,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def pointwise_product(factors, bn):\n",
+ " return reduce(lambda f, g: f.pointwise_product(g, bn), factors)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource pointwise_product"
+ "psource(pointwise_product)"
]
},
{
@@ -861,13 +2539,118 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 44,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " def sum_out(self, var, bn):\n",
+ " """Make a factor eliminating var by summing over its values."""\n",
+ " variables = [X for X in self.variables if X != var]\n",
+ " cpt = {event_values(e, variables): sum(self.p(extend(e, var, val))\n",
+ " for val in bn.variable_values(var))\n",
+ " for e in all_events(variables, bn, {})}\n",
+ " return Factor(variables, cpt)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource Factor.sum_out"
+ "psource(Factor.sum_out)"
]
},
{
@@ -879,13 +2662,118 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 45,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def sum_out(var, factors, bn):\n",
+ " """Eliminate var from all factors by summing over its values."""\n",
+ " result, var_factors = [], []\n",
+ " for f in factors:\n",
+ " (var_factors if var in f.variables else result).append(f)\n",
+ " result.append(pointwise_product(var_factors, bn).sum_out(var, bn))\n",
+ " return result\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource sum_out"
+ "psource(sum_out)"
]
},
{
@@ -910,26 +2798,226 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 46,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def elimination_ask(X, e, bn):\n",
+ " """Compute bn's P(X|e) by variable elimination. [Figure 14.11]\n",
+ " >>> elimination_ask('Burglary', dict(JohnCalls=T, MaryCalls=T), burglary\n",
+ " ... ).show_approx()\n",
+ " 'False: 0.716, True: 0.284'"""\n",
+ " assert X not in e, "Query variable must be distinct from evidence"\n",
+ " factors = []\n",
+ " for var in reversed(bn.variables):\n",
+ " factors.append(make_factor(var, e, bn))\n",
+ " if is_hidden(var, X, e):\n",
+ " factors = sum_out(var, factors, bn)\n",
+ " return pointwise_product(factors, bn).normalize()\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource elimination_ask"
+ "psource(elimination_ask)"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 47,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'False: 0.716, True: 0.284'"
+ ]
+ },
+ "execution_count": 47,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "elimination_ask('Burglary', dict(JohnCalls=True, MaryCalls=True), burglary).show_approx()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Elimination Ask Optimizations\n",
+ "\n",
+ "`elimination_ask` has some critical point to consider and some optimizations could be performed:\n",
+ "\n",
+ "- **Operation on factors**:\n",
+ "\n",
+ " `sum_out` and `pointwise_product` function used in `elimination_ask` is where space and time complexity arise in the variable elimination algorithm (AIMA3e pg. 526).\n",
+ "\n",
+ ">The only trick is to notice that any factor that does not depend on the variable to be summed out can be moved outside the summation.\n",
+ "\n",
+ "- **Variable ordering**:\n",
+ "\n",
+ " Elimination ordering is important, every choice of ordering yields a valid algorithm, but different orderings cause different intermediate factors to be generated during the calculation (AIMA3e pg. 527). In this case the algorithm applies a reversed order.\n",
+ "\n",
+ "> In general, the time and space requirements of variable elimination are dominated by the size of the largest factor constructed during the operation of the algorithm. This in turn is determined by the order of elimination of variables and by the structure of the network. It turns out to be intractable to determine the optimal ordering, but several good heuristics are available. One fairly effective method is a greedy one: eliminate whichever variable minimizes the size of the next factor to be constructed. \n",
+ "\n",
+ "- **Variable relevance**\n",
+ " \n",
+ " Some variables could be irrelevant to resolve a query (i.e. sums to 1). A variable elimination algorithm can therefore remove all these variables before evaluating the query (AIMA3e pg. 528).\n",
+ "\n",
+ "> An optimization is to remove 'every variable that is not an ancestor of a query variable or evidence variable is irrelevant to the query'."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Runtime comparison\n",
+ "Let's see how the runtimes of these two algorithms compare.\n",
+ "We expect variable elimination to outperform enumeration by a large margin as we reduce the number of repetitive calculations significantly."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 48,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "105 µs ± 11.9 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n"
+ ]
+ }
+ ],
"source": [
+ "%%timeit\n",
+ "enumeration_ask('Burglary', dict(JohnCalls=True, MaryCalls=True), burglary).show_approx()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 49,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "262 µs ± 54.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%timeit\n",
"elimination_ask('Burglary', dict(JohnCalls=True, MaryCalls=True), burglary).show_approx()"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In this test case we observe that variable elimination is slower than what we expected. It has something to do with number of threads, how Python tries to optimize things and this happens because the network is very small, with just 5 nodes. The `elimination_ask` has some critical point and some optimizations must be perfomed as seen above.\n",
+ "
\n",
+ "Of course, for more complicated networks, variable elimination will be significantly faster and runtime will drop not just by a constant factor, but by a polynomial factor proportional to the number of nodes, due to the reduction in repeated calculations."
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -941,13 +3029,117 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 50,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ " def sample(self, event):\n",
+ " """Sample from the distribution for this variable conditioned\n",
+ " on event's values for parent_variables. That is, return True/False\n",
+ " at random according with the conditional probability given the\n",
+ " parents."""\n",
+ " return probability(self.p(True, event))\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource BayesNode.sample"
+ "psource(BayesNode.sample)"
]
},
{
@@ -963,13 +3155,118 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 51,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def prior_sample(bn):\n",
+ " """Randomly sample from bn's full joint distribution. The result\n",
+ " is a {variable: value} dict. [Figure 14.13]"""\n",
+ " event = {}\n",
+ " for node in bn.nodes:\n",
+ " event[node.variable] = node.sample(event)\n",
+ " return event\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource prior_sample"
+ "psource(prior_sample)"
]
},
{
@@ -980,15 +3277,25 @@
"\n",
"
\n",
"\n",
- "We store the samples on the observations. Let us find **P(Rain=True)**"
+ "Traversing the graph in topological order is important.\n",
+ "There are two possible topological orderings for this particular directed acyclic graph.\n",
+ "
\n",
+ "1. `Cloudy -> Sprinkler -> Rain -> Wet Grass`\n",
+ "2. `Cloudy -> Rain -> Sprinkler -> Wet Grass`\n",
+ "
\n",
+ "
\n",
+ "We can follow any of the two orderings to sample from the network.\n",
+ "Any ordering other than these two, however, cannot be used.\n",
+ "
\n",
+ "One way to think about this is that `Cloudy` can be seen as a precondition of both `Rain` and `Sprinkler` and just like we have seen in planning, preconditions need to be satisfied before a certain action can be executed.\n",
+ "
\n",
+ "We store the samples on the observations. Let us find **P(Rain=True)** by taking 1000 random samples from the network."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
+ "execution_count": 52,
+ "metadata": {},
"outputs": [],
"source": [
"N = 1000\n",
@@ -1004,10 +3311,8 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
+ "execution_count": 53,
+ "metadata": {},
"outputs": [],
"source": [
"rain_true = [observation for observation in all_observations if observation['Rain'] == True]"
@@ -1022,11 +3327,17 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 54,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0.503\n"
+ ]
+ }
+ ],
"source": [
"answer = len(rain_true) / N\n",
"print(answer)"
@@ -1036,16 +3347,50 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "To evaluate a conditional distribution. We can use a two-step filtering process. We first separate out the variables that are consistent with the evidence. Then for each value of query variable, we can find probabilities. For example to find **P(Cloudy=True | Rain=True)**. We have already filtered out the values consistent with our evidence in **rain_true**. Now we apply a second filtering step on **rain_true** to find **P(Rain=True and Cloudy=True)**"
+ "Sampling this another time might give different results as we have no control over the distribution of the random samples"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 55,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0.519\n"
+ ]
+ }
+ ],
+ "source": [
+ "N = 1000\n",
+ "all_observations = [prior_sample(sprinkler) for x in range(N)]\n",
+ "rain_true = [observation for observation in all_observations if observation['Rain'] == True]\n",
+ "answer = len(rain_true) / N\n",
+ "print(answer)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To evaluate a conditional distribution. We can use a two-step filtering process. We first separate out the variables that are consistent with the evidence. Then for each value of query variable, we can find probabilities. For example to find **P(Cloudy=True | Rain=True)**. We have already filtered out the values consistent with our evidence in **rain_true**. Now we apply a second filtering step on **rain_true** to find **P(Rain=True and Cloudy=True)**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 56,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0.8265895953757225\n"
+ ]
+ }
+ ],
"source": [
"rain_and_cloudy = [observation for observation in rain_true if observation['Cloudy'] == True]\n",
"answer = len(rain_and_cloudy) / len(rain_true)\n",
@@ -1058,18 +3403,156 @@
"source": [
"### Rejection Sampling\n",
"\n",
- "Rejection Sampling is based on an idea similar to what we did just now. First, it generates samples from the prior distribution specified by the network. Then, it rejects all those that do not match the evidence. The function **rejection_sampling** implements the algorithm described by **Figure 14.14**"
+ "Rejection Sampling is based on an idea similar to what we did just now. \n",
+ "First, it generates samples from the prior distribution specified by the network. \n",
+ "Then, it rejects all those that do not match the evidence. \n",
+ "
\n",
+ "Rejection sampling is advantageous only when we know the query beforehand.\n",
+ "While prior sampling generally works for any query, it might fail in some scenarios.\n",
+ "
\n",
+ "Let's say we have a generic Bayesian network and we have evidence `e`, and we want to know how many times a state `A` is true, given evidence `e` is true.\n",
+ "Normally, prior sampling can answer this question, but let's assume that the probability of evidence `e` being true in our actual probability distribution is very small.\n",
+ "In this situation, it might be possible that sampling never encounters a data-point where `e` is true.\n",
+ "If our sampled data has no instance of `e` being true, `P(e) = 0`, and therefore `P(A | e) / P(e) = 0/0`, which is undefined.\n",
+ "We cannot find the required value using this sample.\n",
+ "
\n",
+ "We can definitely increase the number of sample points, but we can never guarantee that we will encounter the case where `e` is non-zero (assuming our actual probability distribution has atleast one case where `e` is true).\n",
+ "To guarantee this, we would have to consider every single data point, which means we lose the speed advantage that approximation provides us and we essentially have to calculate the exact inference model of the Bayesian network.\n",
+ "
\n",
+ "
\n",
+ "Rejection sampling will be useful in this situation, as we already know the query.\n",
+ "
\n",
+ "While sampling from the network, we will reject any sample which is inconsistent with the evidence variables of the given query (in this example, the only evidence variable is `e`).\n",
+ "We will only consider samples that do not violate **any** of the evidence variables.\n",
+ "In this way, we will have enough data with the required evidence to infer queries involving a subset of that evidence.\n",
+ "
\n",
+ "
\n",
+ "The function **rejection_sampling** implements the algorithm described by **Figure 14.14**"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 57,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def rejection_sampling(X, e, bn, N=10000):\n",
+ " """Estimate the probability distribution of variable X given\n",
+ " evidence e in BayesNet bn, using N samples. [Figure 14.14]\n",
+ " Raises a ZeroDivisionError if all the N samples are rejected,\n",
+ " i.e., inconsistent with e.\n",
+ " >>> random.seed(47)\n",
+ " >>> rejection_sampling('Burglary', dict(JohnCalls=T, MaryCalls=T),\n",
+ " ... burglary, 10000).show_approx()\n",
+ " 'False: 0.7, True: 0.3'\n",
+ " """\n",
+ " counts = {x: 0 for x in bn.variable_values(X)} # bold N in [Figure 14.14]\n",
+ " for j in range(N):\n",
+ " sample = prior_sample(bn) # boldface x in [Figure 14.14]\n",
+ " if consistent_with(sample, e):\n",
+ " counts[sample[X]] += 1\n",
+ " return ProbDist(X, counts)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource rejection_sampling"
+ "psource(rejection_sampling)"
]
},
{
@@ -1083,13 +3566,115 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 58,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def consistent_with(event, evidence):\n",
+ " """Is event consistent with the given evidence?"""\n",
+ " return all(evidence.get(k, v) == v\n",
+ " for k, v in event.items())\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource consistent_with"
+ "psource(consistent_with)"
]
},
{
@@ -1101,11 +3686,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 59,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "0.8035019455252919"
+ ]
+ },
+ "execution_count": 59,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"p = rejection_sampling('Cloudy', dict(Rain=True), sprinkler, 1000)\n",
"p[True]"
@@ -1117,6 +3711,7 @@
"source": [
"### Likelihood Weighting\n",
"\n",
+ "Rejection sampling takes a long time to run when the probability of finding consistent evidence is low. It is also slow for larger networks and more evidence variables.\n",
"Rejection sampling tends to reject a lot of samples if our evidence consists of a large number of variables. Likelihood Weighting solves this by fixing the evidence (i.e. not sampling it) and then using weights to make sure that our overall sampling is still consistent.\n",
"\n",
"The pseudocode in **Figure 14.15** is implemented as **likelihood_weighting** and **weighted_sample**."
@@ -1124,13 +3719,124 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 60,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def weighted_sample(bn, e):\n",
+ " """Sample an event from bn that's consistent with the evidence e;\n",
+ " return the event and its weight, the likelihood that the event\n",
+ " accords to the evidence."""\n",
+ " w = 1\n",
+ " event = dict(e) # boldface x in [Figure 14.15]\n",
+ " for node in bn.nodes:\n",
+ " Xi = node.variable\n",
+ " if Xi in e:\n",
+ " w *= node.p(e[Xi], event)\n",
+ " else:\n",
+ " event[Xi] = node.sample(event)\n",
+ " return event, w\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource weighted_sample"
+ "psource(weighted_sample)"
]
},
{
@@ -1145,24 +3851,144 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 61,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "({'Rain': True, 'Cloudy': False, 'Sprinkler': True, 'WetGrass': True}, 0.2)"
+ ]
+ },
+ "execution_count": 61,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"weighted_sample(sprinkler, dict(Rain=True))"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 62,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def likelihood_weighting(X, e, bn, N=10000):\n",
+ " """Estimate the probability distribution of variable X given\n",
+ " evidence e in BayesNet bn. [Figure 14.15]\n",
+ " >>> random.seed(1017)\n",
+ " >>> likelihood_weighting('Burglary', dict(JohnCalls=T, MaryCalls=T),\n",
+ " ... burglary, 10000).show_approx()\n",
+ " 'False: 0.702, True: 0.298'\n",
+ " """\n",
+ " W = {x: 0 for x in bn.variable_values(X)}\n",
+ " for j in range(N):\n",
+ " sample, weight = weighted_sample(bn, e) # boldface x, w in [Figure 14.15]\n",
+ " W[sample[X]] += weight\n",
+ " return ProbDist(X, W)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource likelihood_weighting"
+ "psource(likelihood_weighting)"
]
},
{
@@ -1174,11 +4000,20 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 63,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'False: 0.2, True: 0.8'"
+ ]
+ },
+ "execution_count": 63,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"likelihood_weighting('Cloudy', dict(Rain=True), sprinkler, 200).show_approx()"
]
@@ -1196,13 +4031,124 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
+ "execution_count": 64,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def gibbs_ask(X, e, bn, N=1000):\n",
+ " """[Figure 14.16]"""\n",
+ " assert X not in e, "Query variable must be distinct from evidence"\n",
+ " counts = {x: 0 for x in bn.variable_values(X)} # bold N in [Figure 14.16]\n",
+ " Z = [var for var in bn.variables if var not in e]\n",
+ " state = dict(e) # boldface x in [Figure 14.16]\n",
+ " for Zi in Z:\n",
+ " state[Zi] = random.choice(bn.variable_values(Zi))\n",
+ " for j in range(N):\n",
+ " for Zi in Z:\n",
+ " state[Zi] = markov_blanket_sample(Zi, state, bn)\n",
+ " counts[state[X]] += 1\n",
+ " return ProbDist(X, counts)\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
- "%psource gibbs_ask"
+ "psource(gibbs_ask)"
]
},
{
@@ -1214,39 +4160,2377 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": false
- },
- "outputs": [],
+ "execution_count": 65,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'False: 0.215, True: 0.785'"
+ ]
+ },
+ "execution_count": 65,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
"source": [
"gibbs_ask('Cloudy', dict(Rain=True), sprinkler, 200).show_approx()"
]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
},
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.4.3"
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Runtime analysis\n",
+ "Let's take a look at how much time each algorithm takes."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 66,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "13.2 ms ± 3.45 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%timeit\n",
+ "all_observations = [prior_sample(sprinkler) for x in range(1000)]\n",
+ "rain_true = [observation for observation in all_observations if observation['Rain'] == True]\n",
+ "len([observation for observation in rain_true if observation['Cloudy'] == True]) / len(rain_true)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 67,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "11 ms ± 687 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%timeit\n",
+ "rejection_sampling('Cloudy', dict(Rain=True), sprinkler, 1000)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 68,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "2.12 ms ± 554 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%timeit\n",
+ "likelihood_weighting('Cloudy', dict(Rain=True), sprinkler, 200)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 69,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "14.4 ms ± 2.16 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%timeit\n",
+ "gibbs_ask('Cloudy', dict(Rain=True), sprinkler, 200)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As expected, all algorithms have a very similar runtime.\n",
+ "However, rejection sampling would be a lot faster and more accurate when the probabiliy of finding data-points consistent with the required evidence is small.\n",
+ "
\n",
+ "Likelihood weighting is the fastest out of all as it doesn't involve rejecting samples, but also has a quite high variance."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## HIDDEN MARKOV MODELS"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Often, we need to carry out probabilistic inference on temporal data or a sequence of observations where the order of observations matter.\n",
+ "We require a model similar to a Bayesian Network, but one that grows over time to keep up with the latest evidences.\n",
+ "If you are familiar with the `mdp` module or Markov models in general, you can probably guess that a Markov model might come close to representing our problem accurately.\n",
+ "
\n",
+ "A Markov model is basically a chain-structured Bayesian Network in which there is one state for each time step and each node has an identical probability distribution.\n",
+ "The first node, however, has a different distribution, called the prior distribution which models the initial state of the process.\n",
+ "A state in a Markov model depends only on the previous state and the latest evidence and not on the states before it.\n",
+ "
\n",
+ "A **Hidden Markov Model** or **HMM** is a special case of a Markov model in which the state of the process is described by a single discrete random variable.\n",
+ "The possible values of the variable are the possible states of the world.\n",
+ "
\n",
+ "But what if we want to model a process with two or more state variables?\n",
+ "In that case, we can still fit the process into the HMM framework by redefining our state variables as a single \"megavariable\".\n",
+ "We do this because carrying out inference on HMMs have standard optimized algorithms.\n",
+ "A HMM is very similar to an MDP, but we don't have the option of taking actions like in MDPs, instead, the process carries on as new evidence appears.\n",
+ "
\n",
+ "If a HMM is truncated at a fixed length, it becomes a Bayesian network and general BN inference can be used on it to answer queries.\n",
+ "\n",
+ "Before we start, it will be helpful to understand the structure of a temporal model. We will use the example of the book with the guard and the umbrella. In this example, the state $\\textbf{X}$ is whether it is a rainy day (`X = True`) or not (`X = False`) at Day $\\textbf{t}$. In the sensor or observation model, the observation or evidence $\\textbf{U}$ is whether the professor holds an umbrella (`U = True`) or not (`U = False`) on **Day** $\\textbf{t}$. Based on that, the transition model is \n",
+ "\n",
+ "| $X_{t-1}$ | $X_{t}$ | **P**$(X_{t}| X_{t-1})$| \n",
+ "| ------------- |------------- | ----------------------------------|\n",
+ "| ***${False}$*** | ***${False}$*** | 0.7 |\n",
+ "| ***${False}$*** | ***${True}$*** | 0.3 |\n",
+ "| ***${True}$*** | ***${False}$*** | 0.3 |\n",
+ "| ***${True}$*** | ***${True}$*** | 0.7 |\n",
+ "\n",
+ "And the the sensor model will be,\n",
+ "\n",
+ "| $X_{t}$ | $U_{t}$ | **P**$(U_{t}|X_{t})$| \n",
+ "| :-------------: |:-------------: | :------------------------:|\n",
+ "| ***${False}$*** | ***${True}$*** | 0.2 |\n",
+ "| ***${False}$*** | ***${False}$*** | 0.8 |\n",
+ "| ***${True}$*** | ***${True}$*** | 0.9 |\n",
+ "| ***${True}$*** | ***${False}$*** | 0.1 |\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "HMMs are implemented in the **`HiddenMarkovModel`** class.\n",
+ "Let's have a look."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 70,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "class HiddenMarkovModel:\n",
+ " """A Hidden markov model which takes Transition model and Sensor model as inputs"""\n",
+ "\n",
+ " def __init__(self, transition_model, sensor_model, prior=None):\n",
+ " self.transition_model = transition_model\n",
+ " self.sensor_model = sensor_model\n",
+ " self.prior = prior or [0.5, 0.5]\n",
+ "\n",
+ " def sensor_dist(self, ev):\n",
+ " if ev is True:\n",
+ " return self.sensor_model[0]\n",
+ " else:\n",
+ " return self.sensor_model[1]\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(HiddenMarkovModel)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We instantiate the object **`hmm`** of the class using a list of lists for both the transition and the sensor model."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 71,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "umbrella_transition_model = [[0.7, 0.3], [0.3, 0.7]]\n",
+ "umbrella_sensor_model = [[0.9, 0.2], [0.1, 0.8]]\n",
+ "hmm = HiddenMarkovModel(umbrella_transition_model, umbrella_sensor_model)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The **`sensor_dist()`** method returns a list with the conditional probabilities of the sensor model."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 72,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[0.9, 0.2]"
+ ]
+ },
+ "execution_count": 72,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "hmm.sensor_dist(ev=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now that we have defined an HMM object, our task here is to compute the belief $B_{t}(x)= P(X_{t}|U_{1:t})$ given evidence **U** at each time step **t**.\n",
+ "
\n",
+ "The basic inference tasks that must be solved are:\n",
+ "1. **Filtering**: Computing the posterior probability distribution over the most recent state, given all the evidence up to the current time step.\n",
+ "2. **Prediction**: Computing the posterior probability distribution over the future state.\n",
+ "3. **Smoothing**: Computing the posterior probability distribution over a past state. Smoothing provides a better estimation as it incorporates more evidence.\n",
+ "4. **Most likely explanation**: Finding the most likely sequence of states for a given observation\n",
+ "5. **Learning**: The transition and sensor models can be learnt, if not yet known, just like in an information gathering agent\n",
+ "
\n",
+ "
\n",
+ "\n",
+ "There are three primary methods to carry out inference in Hidden Markov Models:\n",
+ "1. The Forward-Backward algorithm\n",
+ "2. Fixed lag smoothing\n",
+ "3. Particle filtering\n",
+ "\n",
+ "Let's have a look at how we can carry out inference and answer queries based on our umbrella HMM using these algorithms."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### FORWARD-BACKWARD\n",
+ "This is a general algorithm that works for all Markov models, not just HMMs.\n",
+ "In the filtering task (inference) we are given evidence **U** in each time **t** and we want to compute the belief $B_{t}(x)= P(X_{t}|U_{1:t})$. \n",
+ "We can think of it as a three step process:\n",
+ "1. In every step we start with the current belief $P(X_{t}|e_{1:t})$\n",
+ "2. We update it for time\n",
+ "3. We update it for evidence\n",
+ "\n",
+ "The forward algorithm performs the step 2 and 3 at once. It updates, or better say reweights, the initial belief using the transition and the sensor model. Let's see the umbrella example. On **Day 0** no observation is available, and for that reason we will assume that we have equal possibilities to rain or not. In the **`HiddenMarkovModel`** class, the prior probabilities for **Day 0** are by default [0.5, 0.5]. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The observation update is calculated with the **`forward()`** function. Basically, we update our belief using the observation model. The function returns a list with the probabilities of **raining or not** on **Day 1**."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 73,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def forward(HMM, fv, ev):\n",
+ " prediction = vector_add(scalar_vector_product(fv[0], HMM.transition_model[0]),\n",
+ " scalar_vector_product(fv[1], HMM.transition_model[1]))\n",
+ " sensor_dist = HMM.sensor_dist(ev)\n",
+ "\n",
+ " return normalize(element_wise_product(sensor_dist, prediction))\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(forward)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 74,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The probability of raining on day 1 is 0.82\n"
+ ]
+ }
+ ],
+ "source": [
+ "umbrella_prior = [0.5, 0.5]\n",
+ "belief_day_1 = forward(hmm, umbrella_prior, ev=True)\n",
+ "print ('The probability of raining on day 1 is {:.2f}'.format(belief_day_1[0]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In **Day 2** our initial belief is the updated belief of **Day 1**.\n",
+ "Again using the **`forward()`** function we can compute the probability of raining in **Day 2**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 75,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The probability of raining in day 2 is 0.88\n"
+ ]
+ }
+ ],
+ "source": [
+ "belief_day_2 = forward(hmm, belief_day_1, ev=True)\n",
+ "print ('The probability of raining in day 2 is {:.2f}'.format(belief_day_2[0]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In the smoothing part we are interested in computing the distribution over past states given evidence up to the present. Assume that we want to compute the distribution for the time **k**, for $0\\leq k\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def backward(HMM, b, ev):\n",
+ " sensor_dist = HMM.sensor_dist(ev)\n",
+ " prediction = element_wise_product(sensor_dist, b)\n",
+ "\n",
+ " return normalize(vector_add(scalar_vector_product(prediction[0], HMM.transition_model[0]),\n",
+ " scalar_vector_product(prediction[1], HMM.transition_model[1])))\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(backward)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 77,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[0.6272727272727272, 0.37272727272727274]"
+ ]
+ },
+ "execution_count": 77,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "b = [1, 1]\n",
+ "backward(hmm, b, ev=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Some may notice that the result is not the same as in the book. The main reason is that in the book the normalization step is not used. If we want to normalize the result, one can use the **`normalize()`** helper function.\n",
+ "\n",
+ "In order to find the smoothed estimate for raining in **Day k**, we will use the **`forward_backward()`** function. As in the example in the book, the umbrella is observed in both days and the prior distribution is [0.5, 0.5]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 78,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/markdown": [
+ "### AIMA3e\n",
+ "__function__ FORWARD-BACKWARD(__ev__, _prior_) __returns__ a vector of probability distributions \n",
+ " __inputs__: __ev__, a vector of evidence values for steps 1,…,_t_ \n",
+ " _prior_, the prior distribution on the initial state, __P__(__X__0) \n",
+ " __local variables__: __fv__, a vector of forward messages for steps 0,…,_t_ \n",
+ " __b__, a representation of the backward message, initially all 1s \n",
+ " __sv__, a vector of smoothed estimates for steps 1,…,_t_ \n",
+ "\n",
+ " __fv__\\[0\\] ← _prior_ \n",
+ " __for__ _i_ = 1 __to__ _t_ __do__ \n",
+ " __fv__\\[_i_\\] ← FORWARD(__fv__\\[_i_ − 1\\], __ev__\\[_i_\\]) \n",
+ " __for__ _i_ = _t_ __downto__ 1 __do__ \n",
+ " __sv__\\[_i_\\] ← NORMALIZE(__fv__\\[_i_\\] × __b__) \n",
+ " __b__ ← BACKWARD(__b__, __ev__\\[_i_\\]) \n",
+ " __return__ __sv__\n",
+ "\n",
+ "---\n",
+ "__Figure ??__ The forward\\-backward algorithm for smoothing: computing posterior probabilities of a sequence of states given a sequence of observations. The FORWARD and BACKWARD operators are defined by Equations (__??__) and (__??__), respectively."
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 78,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "pseudocode('Forward-Backward')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 79,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The probability of raining in Day 0 is 0.65 and in Day 1 is 0.88\n"
+ ]
+ }
+ ],
+ "source": [
+ "umbrella_prior = [0.5, 0.5]\n",
+ "prob = forward_backward(hmm, ev=[T, T], prior=umbrella_prior)\n",
+ "print ('The probability of raining in Day 0 is {:.2f} and in Day 1 is {:.2f}'.format(prob[0][0], prob[1][0]))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "Since HMMs are represented as single variable systems, we can represent the transition model and sensor model as matrices.\n",
+ "The `forward_backward` algorithm can be easily carried out on this representation (as we have done here) with a time complexity of $O({S}^{2} t)$ where t is the length of the sequence and each step multiplies a vector of size $S$ with a matrix of dimensions $SxS$.\n",
+ "
\n",
+ "Additionally, the forward pass stores $t$ vectors of size $S$ which makes the auxiliary space requirement equivalent to $O(St)$.\n",
+ "
\n",
+ "
\n",
+ "Is there any way we can improve the time or space complexity?\n",
+ "
\n",
+ "Fortunately, the matrix representation of HMM properties allows us to do so.\n",
+ "
\n",
+ "If $f$ and $b$ represent the forward and backward messages respectively, we can modify the smoothing algorithm by first\n",
+ "running the standard forward pass to compute $f_{t:t}$ (forgetting all the intermediate results) and then running\n",
+ "backward pass for both $b$ and $f$ together, using them to compute the smoothed estimate at each step.\n",
+ "This optimization reduces auxlilary space requirement to constant (irrespective of the length of the sequence) provided\n",
+ "the transition matrix is invertible and the sensor model has no zeros (which is sometimes hard to accomplish)\n",
+ "
\n",
+ "
\n",
+ "Let's look at another algorithm, that carries out smoothing in a more optimized way."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### FIXED LAG SMOOTHING\n",
+ "The matrix formulation allows to optimize online smoothing with a fixed lag.\n",
+ "
\n",
+ "Since smoothing can be done in constant, there should exist an algorithm whose time complexity is independent of the length of the lag.\n",
+ "For smoothing a time slice $t - d$ where $d$ is the lag, we need to compute $\\alpha f_{1:t-d}$ x $b_{t-d+1:t}$ incrementally.\n",
+ "
\n",
+ "As we already know, the forward equation is\n",
+ "
\n",
+ "$$f_{1:t+1} = \\alpha O_{t+1}{T}^{T}f_{1:t}$$\n",
+ "
\n",
+ "and the backward equation is\n",
+ "
\n",
+ "$$b_{k+1:t} = TO_{k+1}b_{k+2:t}$$\n",
+ "
\n",
+ "where $T$ and $O$ are the transition and sensor models respectively.\n",
+ "
\n",
+ "For smoothing, the forward message is easy to compute but there exists no simple relation between the backward message of this time step and the one at the previous time step, hence we apply the backward equation $d$ times to get\n",
+ "
\n",
+ "$$b_{t-d+1:t} = \\left ( \\prod_{i=t-d+1}^{t}{TO_i} \\right )b_{t+1:t} = B_{t-d+1:t}1$$\n",
+ "
\n",
+ "where $B_{t-d+1:t}$ is the product of the sequence of $T$ and $O$ matrices.\n",
+ "
\n",
+ "Here's how the `probability` module implements `fixed_lag_smoothing`.\n",
+ "
"
+ ]
},
- "widgets": {
- "state": {},
- "version": "1.1.1"
+ {
+ "cell_type": "code",
+ "execution_count": 80,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "def fixed_lag_smoothing(e_t, HMM, d, ev, t):\n",
+ " """[Figure 15.6]\n",
+ " Smoothing algorithm with a fixed time lag of 'd' steps.\n",
+ " Online algorithm that outputs the new smoothed estimate if observation\n",
+ " for new time step is given."""\n",
+ " ev.insert(0, None)\n",
+ "\n",
+ " T_model = HMM.transition_model\n",
+ " f = HMM.prior\n",
+ " B = [[1, 0], [0, 1]]\n",
+ " evidence = []\n",
+ "\n",
+ " evidence.append(e_t)\n",
+ " O_t = vector_to_diagonal(HMM.sensor_dist(e_t))\n",
+ " if t > d:\n",
+ " f = forward(HMM, f, e_t)\n",
+ " O_tmd = vector_to_diagonal(HMM.sensor_dist(ev[t - d]))\n",
+ " B = matrix_multiplication(inverse_matrix(O_tmd), inverse_matrix(T_model), B, T_model, O_t)\n",
+ " else:\n",
+ " B = matrix_multiplication(B, T_model, O_t)\n",
+ " t += 1\n",
+ "\n",
+ " if t > d:\n",
+ " # always returns a 1x2 matrix\n",
+ " return [normalize(i) for i in matrix_multiplication([f], B)][0]\n",
+ " else:\n",
+ " return None\n",
+ "
\n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "psource(fixed_lag_smoothing)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This algorithm applies `forward` as usual and optimizes the smoothing step by using the equations above.\n",
+ "This optimization could be achieved only because HMM properties can be represented as matrices.\n",
+ "
\n",
+ "`vector_to_diagonal`, `matrix_multiplication` and `inverse_matrix` are matrix manipulation functions to simplify the implementation.\n",
+ "
\n",
+ "`normalize` is used to normalize the output before returning it."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Here's how we can use `fixed_lag_smoothing` for inference on our umbrella HMM."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 81,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "umbrella_transition_model = [[0.7, 0.3], [0.3, 0.7]]\n",
+ "umbrella_sensor_model = [[0.9, 0.2], [0.1, 0.8]]\n",
+ "hmm = HiddenMarkovModel(umbrella_transition_model, umbrella_sensor_model)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Given evidence T, F, T, F and T, we want to calculate the probability distribution for the fourth day with a fixed lag of 2 days.\n",
+ "
\n",
+ "Let `e_t = False`"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 82,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[0.1111111111111111, 0.8888888888888888]"
+ ]
+ },
+ "execution_count": 82,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "e_t = F\n",
+ "evidence = [T, F, T, F, T]\n",
+ "fixed_lag_smoothing(e_t, hmm, d=2, ev=evidence, t=4)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 83,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "[0.9938650306748466, 0.006134969325153394]"
+ ]
+ },
+ "execution_count": 83,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "e_t = T\n",
+ "evidence = [T, T, F, T, T]\n",
+ "fixed_lag_smoothing(e_t, hmm, d=1, ev=evidence, t=4)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We cannot calculate probability distributions when $t$ is less than $d$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 84,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fixed_lag_smoothing(e_t, hmm, d=5, ev=evidence, t=4)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As expected, the output is `None`"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### PARTICLE FILTERING\n",
+ "The filtering problem is too expensive to solve using the previous methods for problems with large or continuous state spaces.\n",
+ "Particle filtering is a method that can solve the same problem but when the state space is a lot larger, where we wouldn't be able to do these computations in a reasonable amount of time as fast, as time goes by, and we want to keep track of things as they happen.\n",
+ "
\n",
+ "The downside is that it is a sampling method and hence isn't accurate, but the more samples we're willing to take, the more accurate we'd get.\n",
+ "
\n",
+ "In this method, instead of keping track of the probability distribution, we will drop particles in a similar proportion at the required regions.\n",
+ "The internal representation of this distribution is usually a list of particles with coordinates in the state-space.\n",
+ "A particle is just a new name for a sample.\n",
+ "\n",
+ "Particle filtering can be divided into four steps:\n",
+ "1. __Initialization__: \n",
+ "If we have some idea about the prior probability distribution, we drop the initial particles accordingly, or else we just drop them uniformly over the state space.\n",
+ "\n",
+ "2. __Forward pass__: \n",
+ "As time goes by and measurements come in, we are going to move the selected particles into the grid squares that makes the most sense in terms of representing the distribution that we are trying to track.\n",
+ "When time goes by, we just loop through all our particles and try to simulate what could happen to each one of them by sampling its next position from the transition model.\n",
+ "This is like prior sampling - samples' frequencies reflect the transition probabilities.\n",
+ "If we have enough samples we are pretty close to exact values.\n",
+ "We work through the list of particles, one particle at a time, all we do is stochastically simulate what the outcome might be.\n",
+ "If we had no dimension of time, and we had no new measurements come in, this would be exactly the same as what we did in prior sampling.\n",
+ "\n",
+ "3. __Reweight__:\n",
+ "As observations come in, don't sample the observations, fix them and downweight the samples based on the evidence just like in likelihood weighting.\n",
+ "$$w(x) = P(e/x)$$\n",
+ "$$B(X) \\propto P(e/X)B'(X)$$\n",
+ "
\n",
+ "As before, the probabilities don't sum to one, since most have been downweighted.\n",
+ "They sum to an approximation of $P(e)$.\n",
+ "To normalize the resulting distribution, we can divide by $P(e)$\n",
+ "
\n",
+ "Likelihood weighting wasn't the best thing for Bayesian networks, because we were not accounting for the incoming evidence so we were getting samples from the prior distribution, in some sense not the right distribution, so we might end up with a lot of particles with low weights. \n",
+ "These samples were very uninformative and the way we fixed it then was by using __Gibbs sampling__.\n",
+ "Theoretically, Gibbs sampling can be run on a HMM, but as we iterated over the process infinitely many times in a Bayesian network, we cannot do that here as we have new incoming evidence and we also need computational cycles to propagate through time.\n",
+ "
\n",
+ "A lot of samples with very low weight and they are not representative of the _actual probability distribution_.\n",
+ "So if we keep running likelihood weighting, we keep propagating the samples with smaller weights and carry out computations for that even though these samples have no significant contribution to the actual probability distribution.\n",
+ "Which is why we require this last step.\n",
+ "\n",
+ "4. __Resample__:\n",
+ "Rather than tracking weighted samples, we _resample_.\n",
+ "We choose from our weighted sample distribution as many times as the number of particles we initially had and we replace these particles too, so that we have a constant number of particles.\n",
+ "This is equivalent to renormalizing the distribution.\n",
+ "The samples with low weight are rarely chosen in the new distribution after resampling.\n",
+ "This newer set of particles after resampling is in some sense more representative of the actual distribution and so we are better allocating our computational cycles.\n",
+ "Now the update is complete for this time step, continue with the next one.\n",
+ "\n",
+ "
\n",
+ "Let's see how this is implemented in the module."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 85,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " Codestin Search App\n",
+ " \n",
+ " \n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "