python sources
parent
09f7f9ed3d
commit
e079ae5ad2
@ -0,0 +1,533 @@
|
||||
"""Implement Agents and Environments (Chapters 1-2).
|
||||
|
||||
The class hierarchies are as follows:
|
||||
|
||||
Object ## A physical object that can exist in an environment
|
||||
Agent
|
||||
Wumpus
|
||||
RandomAgent
|
||||
ReflexVacuumAgent
|
||||
...
|
||||
Dirt
|
||||
Wall
|
||||
...
|
||||
|
||||
Environment ## An environment holds objects, runs simulations
|
||||
XYEnvironment
|
||||
VacuumEnvironment
|
||||
WumpusEnvironment
|
||||
|
||||
EnvFrame ## A graphical representation of the Environment
|
||||
|
||||
"""
|
||||
|
||||
from utils import *
|
||||
import random, copy
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class Object:
|
||||
"""This represents any physical object that can appear in an Environment.
|
||||
You subclass Object to get the objects you want. Each object can have a
|
||||
.__name__ slot (used for output only)."""
|
||||
def __repr__(self):
|
||||
return '<%s>' % getattr(self, '__name__', self.__class__.__name__)
|
||||
|
||||
def is_alive(self):
|
||||
"""Objects that are 'alive' should return true."""
|
||||
return hasattr(self, 'alive') and self.alive
|
||||
|
||||
def display(self, canvas, x, y, width, height):
|
||||
"""Display an image of this Object on the canvas."""
|
||||
pass
|
||||
|
||||
class Agent(Object):
|
||||
"""An Agent is a subclass of Object with one required slot,
|
||||
.program, which should hold a function that takes one argument, the
|
||||
percept, and returns an action. (What counts as a percept or action
|
||||
will depend on the specific environment in which the agent exists.)
|
||||
Note that 'program' is a slot, not a method. If it were a method,
|
||||
then the program could 'cheat' and look at aspects of the agent.
|
||||
It's not supposed to do that: the program can only look at the
|
||||
percepts. An agent program that needs a model of the world (and of
|
||||
the agent itself) will have to build and maintain its own model.
|
||||
There is an optional slots, .performance, which is a number giving
|
||||
the performance measure of the agent in its environment."""
|
||||
|
||||
def __init__(self):
|
||||
def program(percept):
|
||||
return raw_input('Percept=%s; action? ' % percept)
|
||||
self.program = program
|
||||
self.alive = True
|
||||
|
||||
def TraceAgent(agent):
|
||||
"""Wrap the agent's program to print its input and output. This will let
|
||||
you see what the agent is doing in the environment."""
|
||||
old_program = agent.program
|
||||
def new_program(percept):
|
||||
action = old_program(percept)
|
||||
print '%s perceives %s and does %s' % (agent, percept, action)
|
||||
return action
|
||||
agent.program = new_program
|
||||
return agent
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class TableDrivenAgent(Agent):
|
||||
"""This agent selects an action based on the percept sequence.
|
||||
It is practical only for tiny domains.
|
||||
To customize it you provide a table to the constructor. [Fig. 2.7]"""
|
||||
|
||||
def __init__(self, table):
|
||||
"Supply as table a dictionary of all {percept_sequence:action} pairs."
|
||||
## The agent program could in principle be a function, but because
|
||||
## it needs to store state, we make it a callable instance of a class.
|
||||
Agent.__init__(self)
|
||||
percepts = []
|
||||
def program(percept):
|
||||
percepts.append(percept)
|
||||
action = table.get(tuple(percepts))
|
||||
return action
|
||||
self.program = program
|
||||
|
||||
|
||||
class RandomAgent(Agent):
|
||||
"An agent that chooses an action at random, ignoring all percepts."
|
||||
def __init__(self, actions):
|
||||
Agent.__init__(self)
|
||||
self.program = lambda percept: random.choice(actions)
|
||||
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
loc_A, loc_B = (0, 0), (1, 0) # The two locations for the Vacuum world
|
||||
|
||||
class ReflexVacuumAgent(Agent):
|
||||
"A reflex agent for the two-state vacuum environment. [Fig. 2.8]"
|
||||
|
||||
def __init__(self):
|
||||
Agent.__init__(self)
|
||||
def program((location, status)):
|
||||
if status == 'Dirty': return 'Suck'
|
||||
elif location == loc_A: return 'Right'
|
||||
elif location == loc_B: return 'Left'
|
||||
self.program = program
|
||||
|
||||
|
||||
def RandomVacuumAgent():
|
||||
"Randomly choose one of the actions from the vaccum environment."
|
||||
return RandomAgent(['Right', 'Left', 'Suck', 'NoOp'])
|
||||
|
||||
|
||||
def TableDrivenVacuumAgent():
|
||||
"[Fig. 2.3]"
|
||||
table = {((loc_A, 'Clean'),): 'Right',
|
||||
((loc_A, 'Dirty'),): 'Suck',
|
||||
((loc_B, 'Clean'),): 'Left',
|
||||
((loc_B, 'Dirty'),): 'Suck',
|
||||
((loc_A, 'Clean'), (loc_A, 'Clean')): 'Right',
|
||||
((loc_A, 'Clean'), (loc_A, 'Dirty')): 'Suck',
|
||||
# ...
|
||||
((loc_A, 'Clean'), (loc_A, 'Clean'), (loc_A, 'Clean')): 'Right',
|
||||
((loc_A, 'Clean'), (loc_A, 'Clean'), (loc_A, 'Dirty')): 'Suck',
|
||||
# ...
|
||||
}
|
||||
return TableDrivenAgent(table)
|
||||
|
||||
|
||||
class ModelBasedVacuumAgent(Agent):
|
||||
"An agent that keeps track of what locations are clean or dirty."
|
||||
def __init__(self):
|
||||
Agent.__init__(self)
|
||||
model = {loc_A: None, loc_B: None}
|
||||
def program((location, status)):
|
||||
"Same as ReflexVacuumAgent, except if everything is clean, do NoOp"
|
||||
model[location] = status ## Update the model here
|
||||
if model[loc_A] == model[loc_B] == 'Clean': return 'NoOp'
|
||||
elif status == 'Dirty': return 'Suck'
|
||||
elif location == loc_A: return 'Right'
|
||||
elif location == loc_B: return 'Left'
|
||||
self.program = program
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class Environment:
|
||||
"""Abstract class representing an Environment. 'Real' Environment classes
|
||||
inherit from this. Your Environment will typically need to implement:
|
||||
percept: Define the percept that an agent sees.
|
||||
execute_action: Define the effects of executing an action.
|
||||
Also update the agent.performance slot.
|
||||
The environment keeps a list of .objects and .agents (which is a subset
|
||||
of .objects). Each agent has a .performance slot, initialized to 0.
|
||||
Each object has a .location slot, even though some environments may not
|
||||
need this."""
|
||||
|
||||
def __init__(self,):
|
||||
self.objects = []; self.agents = []
|
||||
|
||||
object_classes = [] ## List of classes that can go into environment
|
||||
|
||||
def percept(self, agent):
|
||||
"Return the percept that the agent sees at this point. Override this."
|
||||
abstract
|
||||
|
||||
def execute_action(self, agent, action):
|
||||
"Change the world to reflect this action. Override this."
|
||||
abstract
|
||||
|
||||
def default_location(self, object):
|
||||
"Default location to place a new object with unspecified location."
|
||||
return None
|
||||
|
||||
def exogenous_change(self):
|
||||
"If there is spontaneous change in the world, override this."
|
||||
pass
|
||||
|
||||
def is_done(self):
|
||||
"By default, we're done when we can't find a live agent."
|
||||
for agent in self.agents:
|
||||
if agent.is_alive(): return False
|
||||
return True
|
||||
|
||||
def step(self):
|
||||
"""Run the environment for one time step. If the
|
||||
actions and exogenous changes are independent, this method will
|
||||
do. If there are interactions between them, you'll need to
|
||||
override this method."""
|
||||
if not self.is_done():
|
||||
actions = [agent.program(self.percept(agent))
|
||||
for agent in self.agents]
|
||||
for (agent, action) in zip(self.agents, actions):
|
||||
self.execute_action(agent, action)
|
||||
self.exogenous_change()
|
||||
|
||||
def run(self, steps=1000):
|
||||
"""Run the Environment for given number of time steps."""
|
||||
for step in range(steps):
|
||||
if self.is_done(): return
|
||||
self.step()
|
||||
|
||||
def add_object(self, object, location=None):
|
||||
"""Add an object to the environment, setting its location. Also keep
|
||||
track of objects that are agents. Shouldn't need to override this."""
|
||||
object.location = location or self.default_location(object)
|
||||
self.objects.append(object)
|
||||
if isinstance(object, Agent):
|
||||
object.performance = 0
|
||||
self.agents.append(object)
|
||||
return self
|
||||
|
||||
|
||||
class XYEnvironment(Environment):
|
||||
"""This class is for environments on a 2D plane, with locations
|
||||
labelled by (x, y) points, either discrete or continuous. Agents
|
||||
perceive objects within a radius. Each agent in the environment
|
||||
has a .location slot which should be a location such as (0, 1),
|
||||
and a .holding slot, which should be a list of objects that are
|
||||
held """
|
||||
|
||||
def __init__(self, width=10, height=10):
|
||||
update(self, objects=[], agents=[], width=width, height=height)
|
||||
|
||||
def objects_at(self, location):
|
||||
"Return all objects exactly at a given location."
|
||||
return [obj for obj in self.objects if obj.location == location]
|
||||
|
||||
def objects_near(self, location, radius):
|
||||
"Return all objects within radius of location."
|
||||
radius2 = radius * radius
|
||||
return [obj for obj in self.objects
|
||||
if distance2(location, obj.location) <= radius2]
|
||||
|
||||
def percept(self, agent):
|
||||
"By default, agent perceives objects within radius r."
|
||||
return [self.object_percept(obj, agent)
|
||||
for obj in self.objects_near(agent)]
|
||||
|
||||
def execute_action(self, agent, action):
|
||||
if action == 'TurnRight':
|
||||
agent.heading = turn_heading(agent.heading, -1)
|
||||
elif action == 'TurnLeft':
|
||||
agent.heading = turn_heading(agent.heading, +1)
|
||||
elif action == 'Forward':
|
||||
self.move_to(agent, vector_add(agent.heading, agent.location))
|
||||
elif action == 'Grab':
|
||||
objs = [obj for obj in self.objects_at(agent.location)
|
||||
if obj.is_grabable(agent)]
|
||||
if objs:
|
||||
agent.holding.append(objs[0])
|
||||
elif action == 'Release':
|
||||
if agent.holding:
|
||||
agent.holding.pop()
|
||||
agent.bump = False
|
||||
|
||||
def object_percept(self, obj, agent): #??? Should go to object?
|
||||
"Return the percept for this object."
|
||||
return obj.__class__.__name__
|
||||
|
||||
def default_location(self, object):
|
||||
return (random.choice(self.width), random.choice(self.height))
|
||||
|
||||
def move_to(object, destination):
|
||||
"Move an object to a new location."
|
||||
|
||||
def add_object(self, object, location=(1, 1)):
|
||||
Environment.add_object(self, object, location)
|
||||
object.holding = []
|
||||
object.held = None
|
||||
self.objects.append(object)
|
||||
|
||||
def add_walls(self):
|
||||
"Put walls around the entire perimeter of the grid."
|
||||
for x in range(self.width):
|
||||
self.add_object(Wall(), (x, 0))
|
||||
self.add_object(Wall(), (x, self.height-1))
|
||||
for y in range(self.height):
|
||||
self.add_object(Wall(), (0, y))
|
||||
self.add_object(Wall(), (self.width-1, y))
|
||||
|
||||
def turn_heading(self, heading, inc,
|
||||
headings=[(1, 0), (0, 1), (-1, 0), (0, -1)]):
|
||||
"Return the heading to the left (inc=+1) or right (inc=-1) in headings."
|
||||
return headings[(headings.index(heading) + inc) % len(headings)]
|
||||
|
||||
#______________________________________________________________________________
|
||||
## Vacuum environment
|
||||
|
||||
class TrivialVacuumEnvironment(Environment):
|
||||
"""This environment has two locations, A and B. Each can be Dirty or Clean.
|
||||
The agent perceives its location and the location's status. This serves as
|
||||
an example of how to implement a simple Environment."""
|
||||
|
||||
def __init__(self):
|
||||
Environment.__init__(self)
|
||||
self.status = {loc_A:random.choice(['Clean', 'Dirty']),
|
||||
loc_B:random.choice(['Clean', 'Dirty'])}
|
||||
|
||||
def percept(self, agent):
|
||||
"Returns the agent's location, and the location status (Dirty/Clean)."
|
||||
return (agent.location, self.status[agent.location])
|
||||
|
||||
def execute_action(self, agent, action):
|
||||
"""Change agent's location and/or location's status; track performance.
|
||||
Score 10 for each dirt cleaned; -1 for each move."""
|
||||
if action == 'Right':
|
||||
agent.location = loc_B
|
||||
agent.performance -= 1
|
||||
elif action == 'Left':
|
||||
agent.location = loc_A
|
||||
agent.performance -= 1
|
||||
elif action == 'Suck':
|
||||
if self.status[agent.location] == 'Dirty':
|
||||
agent.performance += 10
|
||||
self.status[agent.location] = 'Clean'
|
||||
|
||||
def default_location(self, object):
|
||||
"Agents start in either location at random."
|
||||
return random.choice([loc_A, loc_B])
|
||||
|
||||
class Dirt(Object): pass
|
||||
class Wall(Object): pass
|
||||
|
||||
class VacuumEnvironment(XYEnvironment):
|
||||
"""The environment of [Ex. 2.12]. Agent perceives dirty or clean,
|
||||
and bump (into obstacle) or not; 2D discrete world of unknown size;
|
||||
performance measure is 100 for each dirt cleaned, and -1 for
|
||||
each turn taken."""
|
||||
def __init__(self, width=10, height=10):
|
||||
XYEnvironment.__init__(self, width, height)
|
||||
self.add_walls()
|
||||
|
||||
object_classes = [Wall, Dirt, ReflexVacuumAgent, RandomVacuumAgent,
|
||||
TableDrivenVacuumAgent, ModelBasedVacuumAgent]
|
||||
|
||||
def percept(self, agent):
|
||||
"""The percept is a tuple of ('Dirty' or 'Clean', 'Bump' or 'None').
|
||||
Unlike the TrivialVacuumEnvironment, location is NOT perceived."""
|
||||
status = if_(self.find_at(Dirt, agent.location), 'Dirty', 'Clean')
|
||||
bump = if_(agent.bump, 'Bump', 'None')
|
||||
return (status, bump)
|
||||
|
||||
def execute_action(self, agent, action):
|
||||
if action == 'Suck':
|
||||
if self.find_at(Dirt, agent.location):
|
||||
agent.performance += 100
|
||||
agent.performance -= 1
|
||||
XYEnvironment.execute_action(self, agent, action)
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class SimpleReflexAgent(Agent):
|
||||
"""This agent takes action based solely on the percept. [Fig. 2.13]"""
|
||||
|
||||
def __init__(self, rules, interpret_input):
|
||||
Agent.__init__(self)
|
||||
def program(percept):
|
||||
state = interpret_input(percept)
|
||||
rule = rule_match(state, rules)
|
||||
action = rule.action
|
||||
return action
|
||||
self.program = program
|
||||
|
||||
class ReflexAgentWithState(Agent):
|
||||
"""This agent takes action based on the percept and state. [Fig. 2.16]"""
|
||||
|
||||
def __init__(self, rules, udpate_state):
|
||||
Agent.__init__(self)
|
||||
state, action = None, None
|
||||
def program(percept):
|
||||
state = update_state(state, action, percept)
|
||||
rule = rule_match(state, rules)
|
||||
action = rule.action
|
||||
return action
|
||||
self.program = program
|
||||
|
||||
#______________________________________________________________________________
|
||||
## The Wumpus World
|
||||
|
||||
class Gold(Object): pass
|
||||
class Pit(Object): pass
|
||||
class Arrow(Object): pass
|
||||
class Wumpus(Agent): pass
|
||||
class Explorer(Agent): pass
|
||||
|
||||
class WumpusEnvironment(XYEnvironment):
|
||||
object_classes = [Wall, Gold, Pit, Arrow, Wumpus, Explorer]
|
||||
def __init__(self, width=10, height=10):
|
||||
XYEnvironment.__init__(self, width, height)
|
||||
self.add_walls()
|
||||
## Needs a lot of work ...
|
||||
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
def compare_agents(EnvFactory, AgentFactories, n=10, steps=1000):
|
||||
"""See how well each of several agents do in n instances of an environment.
|
||||
Pass in a factory (constructor) for environments, and several for agents.
|
||||
Create n instances of the environment, and run each agent in copies of
|
||||
each one for steps. Return a list of (agent, average-score) tuples."""
|
||||
envs = [EnvFactory() for i in range(n)]
|
||||
return [(A, test_agent(A, steps, copy.deepcopy(envs)))
|
||||
for A in AgentFactories]
|
||||
|
||||
def test_agent(AgentFactory, steps, envs):
|
||||
"Return the mean score of running an agent in each of the envs, for steps"
|
||||
total = 0
|
||||
for env in envs:
|
||||
agent = AgentFactory()
|
||||
env.add_object(agent)
|
||||
env.run(steps)
|
||||
total += agent.performance
|
||||
return float(total)/len(envs)
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
_docex = """
|
||||
a = ReflexVacuumAgent()
|
||||
a.program
|
||||
a.program((loc_A, 'Clean')) ==> 'Right'
|
||||
a.program((loc_B, 'Clean')) ==> 'Left'
|
||||
a.program((loc_A, 'Dirty')) ==> 'Suck'
|
||||
a.program((loc_A, 'Dirty')) ==> 'Suck'
|
||||
|
||||
e = TrivialVacuumEnvironment()
|
||||
e.add_object(TraceAgent(ModelBasedVacuumAgent()))
|
||||
e.run(5)
|
||||
|
||||
## Environments, and some agents, are randomized, so the best we can
|
||||
## give is a range of expected scores. If this test fails, it does
|
||||
## not necessarily mean something is wrong.
|
||||
envs = [TrivialVacuumEnvironment() for i in range(100)]
|
||||
def testv(A): return test_agent(A, 4, copy.deepcopy(envs))
|
||||
testv(ModelBasedVacuumAgent)
|
||||
(7 < _ < 11) ==> True
|
||||
testv(ReflexVacuumAgent)
|
||||
(5 < _ < 9) ==> True
|
||||
testv(TableDrivenVacuumAgent)
|
||||
(2 < _ < 6) ==> True
|
||||
testv(RandomVacuumAgent)
|
||||
(0.5 < _ < 3) ==> True
|
||||
"""
|
||||
|
||||
#______________________________________________________________________________
|
||||
# GUI - Graphical User Interface for Environments
|
||||
# If you do not have Tkinter installed, either get a new installation of Python
|
||||
# (Tkinter is standard in all new releases), or delete the rest of this file
|
||||
# and muddle through without a GUI.
|
||||
|
||||
'''
|
||||
import Tkinter as tk
|
||||
|
||||
class EnvFrame(tk.Frame):
|
||||
def __init__(self, env, title='AIMA GUI', cellwidth=50, n=10):
|
||||
update(self, cellwidth = cellwidth, running=False, delay=1.0)
|
||||
self.n = n
|
||||
self.running = 0
|
||||
self.delay = 1.0
|
||||
self.env = env
|
||||
tk.Frame.__init__(self, None, width=(cellwidth+2)*n, height=(cellwidth+2)*n)
|
||||
#self.title(title)
|
||||
# Toolbar
|
||||
toolbar = tk.Frame(self, relief='raised', bd=2)
|
||||
toolbar.pack(side='top', fill='x')
|
||||
for txt, cmd in [('Step >', self.env.step), ('Run >>', self.run),
|
||||
('Stop [ ]', self.stop)]:
|
||||
tk.Button(toolbar, text=txt, command=cmd).pack(side='left')
|
||||
tk.Label(toolbar, text='Delay').pack(side='left')
|
||||
scale = tk.Scale(toolbar, orient='h', from_=0.0, to=10, resolution=0.5,
|
||||
command=lambda d: setattr(self, 'delay', d))
|
||||
scale.set(self.delay)
|
||||
scale.pack(side='left')
|
||||
# Canvas for drawing on
|
||||
self.canvas = tk.Canvas(self, width=(cellwidth+1)*n,
|
||||
height=(cellwidth+1)*n, background="white")
|
||||
self.canvas.bind('<Button-1>', self.left) ## What should this do?
|
||||
self.canvas.bind('<Button-2>', self.edit_objects)
|
||||
self.canvas.bind('<Button-3>', self.add_object)
|
||||
if cellwidth:
|
||||
c = self.canvas
|
||||
for i in range(1, n+1):
|
||||
c.create_line(0, i*cellwidth, n*cellwidth, i*cellwidth)
|
||||
c.create_line(i*cellwidth, 0, i*cellwidth, n*cellwidth)
|
||||
c.pack(expand=1, fill='both')
|
||||
self.pack()
|
||||
|
||||
|
||||
def background_run(self):
|
||||
if self.running:
|
||||
self.env.step()
|
||||
ms = int(1000 * max(float(self.delay), 0.5))
|
||||
self.after(ms, self.background_run)
|
||||
|
||||
def run(self):
|
||||
print 'run'
|
||||
self.running = 1
|
||||
self.background_run()
|
||||
|
||||
def stop(self):
|
||||
print 'stop'
|
||||
self.running = 0
|
||||
|
||||
def left(self, event):
|
||||
print 'left at ', event.x/50, event.y/50
|
||||
|
||||
def edit_objects(self, event):
|
||||
"""Choose an object within radius and edit its fields."""
|
||||
pass
|
||||
|
||||
def add_object(self, event):
|
||||
## This is supposed to pop up a menu of Object classes; you choose the one
|
||||
## You want to put in this square. Not working yet.
|
||||
menu = tk.Menu(self, title='Edit (%d, %d)' % (event.x/50, event.y/50))
|
||||
for (txt, cmd) in [('Wumpus', self.run), ('Pit', self.run)]:
|
||||
menu.add_command(label=txt, command=cmd)
|
||||
menu.tk_popup(event.x + self.winfo_rootx(),
|
||||
event.y + self.winfo_rooty())
|
||||
|
||||
#image=PhotoImage(file=r"C:\Documents and Settings\pnorvig\Desktop\wumpus.gif")
|
||||
#self.images = []
|
||||
#self.images.append(image)
|
||||
#c.create_image(200,200,anchor=NW,image=image)
|
||||
|
||||
#v = VacuumEnvironment(); w = EnvFrame(v);
|
||||
'''
|
@ -0,0 +1,450 @@
|
||||
"""CSP (Constraint Satisfaction Problems) problems and solvers. (Chapter 5)."""
|
||||
|
||||
from __future__ import generators
|
||||
from utils import *
|
||||
import search
|
||||
import types
|
||||
|
||||
class CSP(search.Problem):
|
||||
"""This class describes finite-domain Constraint Satisfaction Problems.
|
||||
A CSP is specified by the following three inputs:
|
||||
vars A list of variables; each is atomic (e.g. int or string).
|
||||
domains A dict of {var:[possible_value, ...]} entries.
|
||||
neighbors A dict of {var:[var,...]} that for each variable lists
|
||||
the other variables that participate in constraints.
|
||||
constraints A function f(A, a, B, b) that returns true if neighbors
|
||||
A, B satisfy the constraint when they have values A=a, B=b
|
||||
In the textbook and in most mathematical definitions, the
|
||||
constraints are specified as explicit pairs of allowable values,
|
||||
but the formulation here is easier to express and more compact for
|
||||
most cases. (For example, the n-Queens problem can be represented
|
||||
in O(n) space using this notation, instead of O(N^4) for the
|
||||
explicit representation.) In terms of describing the CSP as a
|
||||
problem, that's all there is.
|
||||
|
||||
However, the class also supports data structures and methods that help you
|
||||
solve CSPs by calling a search function on the CSP. Methods and slots are
|
||||
as follows, where the argument 'a' represents an assignment, which is a
|
||||
dict of {var:val} entries:
|
||||
assign(var, val, a) Assign a[var] = val; do other bookkeeping
|
||||
unassign(var, a) Do del a[var], plus other bookkeeping
|
||||
nconflicts(var, val, a) Return the number of other variables that
|
||||
conflict with var=val
|
||||
curr_domains[var] Slot: remaining consistent values for var
|
||||
Used by constraint propagation routines.
|
||||
The following methods are used only by graph_search and tree_search:
|
||||
succ() Return a list of (action, state) pairs
|
||||
goal_test(a) Return true if all constraints satisfied
|
||||
The following are just for debugging purposes:
|
||||
nassigns Slot: tracks the number of assignments made
|
||||
display(a) Print a human-readable representation
|
||||
"""
|
||||
|
||||
def __init__(self, vars, domains, neighbors, constraints):
|
||||
"Construct a CSP problem. If vars is empty, it becomes domains.keys()."
|
||||
vars = vars or domains.keys()
|
||||
update(self, vars=vars, domains=domains,
|
||||
neighbors=neighbors, constraints=constraints,
|
||||
initial={}, curr_domains=None, pruned=None, nassigns=0)
|
||||
|
||||
def assign(self, var, val, assignment):
|
||||
"""Add {var: val} to assignment; Discard the old value if any.
|
||||
Do bookkeeping for curr_domains and nassigns."""
|
||||
self.nassigns += 1
|
||||
assignment[var] = val
|
||||
if self.curr_domains:
|
||||
if self.fc:
|
||||
self.forward_check(var, val, assignment)
|
||||
if self.mac:
|
||||
AC3(self, [(Xk, var) for Xk in self.neighbors[var]])
|
||||
|
||||
def unassign(self, var, assignment):
|
||||
"""Remove {var: val} from assignment; that is backtrack.
|
||||
DO NOT call this if you are changing a variable to a new value;
|
||||
just call assign for that."""
|
||||
if var in assignment:
|
||||
# Reset the curr_domain to be the full original domain
|
||||
if self.curr_domains:
|
||||
self.curr_domains[var] = self.domains[var][:]
|
||||
del assignment[var]
|
||||
|
||||
def nconflicts(self, var, val, assignment):
|
||||
"Return the number of conflicts var=val has with other variables."
|
||||
# Subclasses may implement this more efficiently
|
||||
def conflict(var2):
|
||||
val2 = assignment.get(var2, None)
|
||||
return val2 != None and not self.constraints(var, val, var2, val2)
|
||||
return count_if(conflict, self.neighbors[var])
|
||||
|
||||
def forward_check(self, var, val, assignment):
|
||||
"Do forward checking (current domain reduction) for this assignment."
|
||||
if self.curr_domains:
|
||||
# Restore prunings from previous value of var
|
||||
for (B, b) in self.pruned[var]:
|
||||
self.curr_domains[B].append(b)
|
||||
self.pruned[var] = []
|
||||
# Prune any other B=b assignement that conflict with var=val
|
||||
for B in self.neighbors[var]:
|
||||
if B not in assignment:
|
||||
for b in self.curr_domains[B][:]:
|
||||
if not self.constraints(var, val, B, b):
|
||||
self.curr_domains[B].remove(b)
|
||||
self.pruned[var].append((B, b))
|
||||
|
||||
def display(self, assignment):
|
||||
"Show a human-readable representation of the CSP."
|
||||
# Subclasses can print in a prettier way, or display with a GUI
|
||||
print 'CSP:', self, 'with assignment:', assignment
|
||||
|
||||
## These methods are for the tree and graph search interface:
|
||||
|
||||
def succ(self, assignment):
|
||||
"Return a list of (action, state) pairs."
|
||||
if len(assignment) == len(self.vars):
|
||||
return []
|
||||
else:
|
||||
var = find_if(lambda v: v not in assignment, self.vars)
|
||||
result = []
|
||||
for val in self.domains[var]:
|
||||
if self.nconflicts(self, var, val, assignment) == 0:
|
||||
a = assignment.copy; a[var] = val
|
||||
result.append(((var, val), a))
|
||||
return result
|
||||
|
||||
def goal_test(self, assignment):
|
||||
"The goal is to assign all vars, with all constraints satisfied."
|
||||
return (len(assignment) == len(self.vars) and
|
||||
every(lambda var: self.nconflicts(var, assignment[var],
|
||||
assignment) == 0,
|
||||
self.vars))
|
||||
|
||||
## This is for min_conflicts search
|
||||
|
||||
def conflicted_vars(self, current):
|
||||
"Return a list of variables in current assignment that are in conflict"
|
||||
return [var for var in self.vars
|
||||
if self.nconflicts(var, current[var], current) > 0]
|
||||
|
||||
#______________________________________________________________________________
|
||||
# CSP Backtracking Search
|
||||
|
||||
def backtracking_search(csp, mcv=False, lcv=False, fc=False, mac=False):
|
||||
"""Set up to do recursive backtracking search. Allow the following options:
|
||||
mcv - If true, use Most Constrained Variable Heuristic
|
||||
lcv - If true, use Least Constraining Value Heuristic
|
||||
fc - If true, use Forward Checking
|
||||
mac - If true, use Maintaining Arc Consistency. [Fig. 5.3]
|
||||
>>> backtracking_search(australia)
|
||||
{'WA': 'B', 'Q': 'B', 'T': 'B', 'V': 'B', 'SA': 'G', 'NT': 'R', 'NSW': 'R'}
|
||||
"""
|
||||
if fc or mac:
|
||||
csp.curr_domains, csp.pruned = {}, {}
|
||||
for v in csp.vars:
|
||||
csp.curr_domains[v] = csp.domains[v][:]
|
||||
csp.pruned[v] = []
|
||||
update(csp, mcv=mcv, lcv=lcv, fc=fc, mac=mac)
|
||||
return recursive_backtracking({}, csp)
|
||||
|
||||
def recursive_backtracking(assignment, csp):
|
||||
"""Search for a consistent assignment for the csp.
|
||||
Each recursive call chooses a variable, and considers values for it."""
|
||||
if len(assignment) == len(csp.vars):
|
||||
return assignment
|
||||
var = select_unassigned_variable(assignment, csp)
|
||||
for val in order_domain_values(var, assignment, csp):
|
||||
if csp.fc or csp.nconflicts(var, val, assignment) == 0:
|
||||
csp.assign(var, val, assignment)
|
||||
result = recursive_backtracking(assignment, csp)
|
||||
if result is not None:
|
||||
return result
|
||||
csp.unassign(var, assignment)
|
||||
return None
|
||||
|
||||
def select_unassigned_variable(assignment, csp):
|
||||
"Select the variable to work on next. Find"
|
||||
if csp.mcv: # Most Constrained Variable
|
||||
unassigned = [v for v in csp.vars if v not in assignment]
|
||||
return argmin_random_tie(unassigned,
|
||||
lambda var: -num_legal_values(csp, var, assignment))
|
||||
else: # First unassigned variable
|
||||
for v in csp.vars:
|
||||
if v not in assignment:
|
||||
return v
|
||||
|
||||
def order_domain_values(var, assignment, csp):
|
||||
"Decide what order to consider the domain variables."
|
||||
if csp.curr_domains:
|
||||
domain = csp.curr_domains[var]
|
||||
else:
|
||||
domain = csp.domains[var][:]
|
||||
if csp.lcv:
|
||||
# If LCV is specified, consider values with fewer conflicts first
|
||||
key = lambda val: csp.nconflicts(var, val, assignment)
|
||||
domain.sort(lambda(x,y): cmp(key(x), key(y)))
|
||||
while domain:
|
||||
yield domain.pop()
|
||||
|
||||
def num_legal_values(csp, var, assignment):
|
||||
if csp.curr_domains:
|
||||
return len(csp.curr_domains[var])
|
||||
else:
|
||||
return count_if(lambda val: csp.nconflicts(var, val, assignment) == 0,
|
||||
csp.domains[var])
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Constraint Propagation with AC-3
|
||||
|
||||
def AC3(csp, queue=None):
|
||||
"""[Fig. 5.7]"""
|
||||
if queue == None:
|
||||
queue = [(Xi, Xk) for Xi in csp.vars for Xk in csp.neighbors[Xi]]
|
||||
while queue:
|
||||
(Xi, Xj) = queue.pop()
|
||||
if remove_inconsistent_values(csp, Xi, Xj):
|
||||
for Xk in csp.neighbors[Xi]:
|
||||
queue.append((Xk, Xi))
|
||||
|
||||
def remove_inconsistent_values(csp, Xi, Xj):
|
||||
"Return true if we remove a value."
|
||||
removed = False
|
||||
for x in csp.curr_domains[Xi][:]:
|
||||
# If Xi=x conflicts with Xj=y for every possible y, eliminate Xi=x
|
||||
if every(lambda y: not csp.constraints(Xi, x, Xj, y),
|
||||
csp.curr_domains[Xj]):
|
||||
csp.curr_domains[Xi].remove(x)
|
||||
removed = True
|
||||
return removed
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Min-conflicts hillclimbing search for CSPs
|
||||
|
||||
def min_conflicts(csp, max_steps=1000000):
|
||||
"""Solve a CSP by stochastic hillclimbing on the number of conflicts."""
|
||||
# Generate a complete assignement for all vars (probably with conflicts)
|
||||
current = {}; csp.current = current
|
||||
for var in csp.vars:
|
||||
val = min_conflicts_value(csp, var, current)
|
||||
csp.assign(var, val, current)
|
||||
# Now repeapedly choose a random conflicted variable and change it
|
||||
for i in range(max_steps):
|
||||
conflicted = csp.conflicted_vars(current)
|
||||
if not conflicted:
|
||||
return current
|
||||
var = random.choice(conflicted)
|
||||
val = min_conflicts_value(csp, var, current)
|
||||
csp.assign(var, val, current)
|
||||
return None
|
||||
|
||||
def min_conflicts_value(csp, var, current):
|
||||
"""Return the value that will give var the least number of conflicts.
|
||||
If there is a tie, choose at random."""
|
||||
return argmin_random_tie(csp.domains[var],
|
||||
lambda val: csp.nconflicts(var, val, current))
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Map-Coloring Problems
|
||||
|
||||
class UniversalDict:
|
||||
"""A universal dict maps any key to the same value. We use it here
|
||||
as the domains dict for CSPs in which all vars have the same domain.
|
||||
>>> d = UniversalDict(42)
|
||||
>>> d['life']
|
||||
42
|
||||
"""
|
||||
def __init__(self, value): self.value = value
|
||||
def __getitem__(self, key): return self.value
|
||||
def __repr__(self): return '{Any: %r}' % self.value
|
||||
|
||||
def different_values_constraint(A, a, B, b):
|
||||
"A constraint saying two neighboring variables must differ in value."
|
||||
return a != b
|
||||
|
||||
def MapColoringCSP(colors, neighbors):
|
||||
"""Make a CSP for the problem of coloring a map with different colors
|
||||
for any two adjacent regions. Arguments are a list of colors, and a
|
||||
dict of {region: [neighbor,...]} entries. This dict may also be
|
||||
specified as a string of the form defined by parse_neighbors"""
|
||||
|
||||
if isinstance(neighbors, str):
|
||||
neighbors = parse_neighbors(neighbors)
|
||||
return CSP(neighbors.keys(), UniversalDict(colors), neighbors,
|
||||
different_values_constraint)
|
||||
|
||||
def parse_neighbors(neighbors, vars=[]):
|
||||
"""Convert a string of the form 'X: Y Z; Y: Z' into a dict mapping
|
||||
regions to neighbors. The syntax is a region name followed by a ':'
|
||||
followed by zero or more region names, followed by ';', repeated for
|
||||
each region name. If you say 'X: Y' you don't need 'Y: X'.
|
||||
>>> parse_neighbors('X: Y Z; Y: Z')
|
||||
{'Y': ['X', 'Z'], 'X': ['Y', 'Z'], 'Z': ['X', 'Y']}
|
||||
"""
|
||||
dict = DefaultDict([])
|
||||
for var in vars:
|
||||
dict[var] = []
|
||||
specs = [spec.split(':') for spec in neighbors.split(';')]
|
||||
for (A, Aneighbors) in specs:
|
||||
A = A.strip();
|
||||
dict.setdefault(A, [])
|
||||
for B in Aneighbors.split():
|
||||
dict[A].append(B)
|
||||
dict[B].append(A)
|
||||
return dict
|
||||
|
||||
australia = MapColoringCSP(list('RGB'),
|
||||
'SA: WA NT Q NSW V; NT: WA Q; NSW: Q V; T: ')
|
||||
|
||||
usa = MapColoringCSP(list('RGBY'),
|
||||
"""WA: OR ID; OR: ID NV CA; CA: NV AZ; NV: ID UT AZ; ID: MT WY UT;
|
||||
UT: WY CO AZ; MT: ND SD WY; WY: SD NE CO; CO: NE KA OK NM; NM: OK TX;
|
||||
ND: MN SD; SD: MN IA NE; NE: IA MO KA; KA: MO OK; OK: MO AR TX;
|
||||
TX: AR LA; MN: WI IA; IA: WI IL MO; MO: IL KY TN AR; AR: MS TN LA;
|
||||
LA: MS; WI: MI IL; IL: IN; IN: KY; MS: TN AL; AL: TN GA FL; MI: OH;
|
||||
OH: PA WV KY; KY: WV VA TN; TN: VA NC GA; GA: NC SC FL;
|
||||
PA: NY NJ DE MD WV; WV: MD VA; VA: MD DC NC; NC: SC; NY: VT MA CA NJ;
|
||||
NJ: DE; DE: MD; MD: DC; VT: NH MA; MA: NH RI CT; CT: RI; ME: NH;
|
||||
HI: ; AK: """)
|
||||
#______________________________________________________________________________
|
||||
# n-Queens Problem
|
||||
|
||||
def queen_constraint(A, a, B, b):
|
||||
"""Constraint is satisfied (true) if A, B are really the same variable,
|
||||
or if they are not in the same row, down diagonal, or up diagonal."""
|
||||
return A == B or (a != b and A + a != B + b and A - a != B - b)
|
||||
|
||||
class NQueensCSP(CSP):
|
||||
"""Make a CSP for the nQueens problem for search with min_conflicts.
|
||||
Suitable for large n, it uses only data structures of size O(n).
|
||||
Think of placing queens one per column, from left to right.
|
||||
That means position (x, y) represents (var, val) in the CSP.
|
||||
The main structures are three arrays to count queens that could conflict:
|
||||
rows[i] Number of queens in the ith row (i.e val == i)
|
||||
downs[i] Number of queens in the \ diagonal
|
||||
such that their (x, y) coordinates sum to i
|
||||
ups[i] Number of queens in the / diagonal
|
||||
such that their (x, y) coordinates have x-y+n-1 = i
|
||||
We increment/decrement these counts each time a queen is placed/moved from
|
||||
a row/diagonal. So moving is O(1), as is nconflicts. But choosing
|
||||
a variable, and a best value for the variable, are each O(n).
|
||||
If you want, you can keep track of conflicted vars, then variable
|
||||
selection will also be O(1).
|
||||
>>> len(backtracking_search(NQueensCSP(8)))
|
||||
8
|
||||
>>> len(min_conflicts(NQueensCSP(8)))
|
||||
8
|
||||
"""
|
||||
def __init__(self, n):
|
||||
"""Initialize data structures for n Queens."""
|
||||
CSP.__init__(self, range(n), UniversalDict(range(n)),
|
||||
UniversalDict(range(n)), queen_constraint)
|
||||
update(self, rows=[0]*n, ups=[0]*(2*n - 1), downs=[0]*(2*n - 1))
|
||||
|
||||
def nconflicts(self, var, val, assignment):
|
||||
"""The number of conflicts, as recorded with each assignment.
|
||||
Count conflicts in row and in up, down diagonals. If there
|
||||
is a queen there, it can't conflict with itself, so subtract 3."""
|
||||
n = len(self.vars)
|
||||
c = self.rows[val] + self.downs[var+val] + self.ups[var-val+n-1]
|
||||
if assignment.get(var, None) == val:
|
||||
c -= 3
|
||||
return c
|
||||
|
||||
def assign(self, var, val, assignment):
|
||||
"Assign var, and keep track of conflicts."
|
||||
oldval = assignment.get(var, None)
|
||||
if val != oldval:
|
||||
if oldval is not None: # Remove old val if there was one
|
||||
self.record_conflict(assignment, var, oldval, -1)
|
||||
self.record_conflict(assignment, var, val, +1)
|
||||
CSP.assign(self, var, val, assignment)
|
||||
|
||||
def unassign(self, var, assignment):
|
||||
"Remove var from assignment (if it is there) and track conflicts."
|
||||
if var in assignment:
|
||||
self.record_conflict(assignment, var, assignment[var], -1)
|
||||
CSP.unassign(self, var, assignment)
|
||||
|
||||
def record_conflict(self, assignment, var, val, delta):
|
||||
"Record conflicts caused by addition or deletion of a Queen."
|
||||
n = len(self.vars)
|
||||
self.rows[val] += delta
|
||||
self.downs[var + val] += delta
|
||||
self.ups[var - val + n - 1] += delta
|
||||
|
||||
def display(self, assignment):
|
||||
"Print the queens and the nconflicts values (for debugging)."
|
||||
n = len(self.vars)
|
||||
for val in range(n):
|
||||
for var in range(n):
|
||||
if assignment.get(var,'') == val: ch ='Q'
|
||||
elif (var+val) % 2 == 0: ch = '.'
|
||||
else: ch = '-'
|
||||
print ch,
|
||||
print ' ',
|
||||
for var in range(n):
|
||||
if assignment.get(var,'') == val: ch ='*'
|
||||
else: ch = ' '
|
||||
print str(self.nconflicts(var, val, assignment))+ch,
|
||||
print
|
||||
|
||||
#______________________________________________________________________________
|
||||
# The Zebra Puzzle
|
||||
|
||||
def Zebra():
|
||||
"Return an instance of the Zebra Puzzle."
|
||||
Colors = 'Red Yellow Blue Green Ivory'.split()
|
||||
Pets = 'Dog Fox Snails Horse Zebra'.split()
|
||||
Drinks = 'OJ Tea Coffee Milk Water'.split()
|
||||
Countries = 'Englishman Spaniard Norwegian Ukranian Japanese'.split()
|
||||
Smokes = 'Kools Chesterfields Winston LuckyStrike Parliaments'.split()
|
||||
vars = Colors + Pets + Drinks + Countries + Smokes
|
||||
domains = {}
|
||||
for var in vars:
|
||||
domains[var] = range(1, 6)
|
||||
domains['Norwegian'] = [1]
|
||||
domains['Milk'] = [3]
|
||||
neighbors = parse_neighbors("""Englishman: Red;
|
||||
Spaniard: Dog; Kools: Yellow; Chesterfields: Fox;
|
||||
Norwegian: Blue; Winston: Snails; LuckyStrike: OJ;
|
||||
Ukranian: Tea; Japanese: Parliaments; Kools: Horse;
|
||||
Coffee: Green; Green: Ivory""", vars)
|
||||
for type in [Colors, Pets, Drinks, Countries, Smokes]:
|
||||
for A in type:
|
||||
for B in type:
|
||||
if A != B:
|
||||
if B not in neighbors[A]: neighbors[A].append(B)
|
||||
if A not in neighbors[B]: neighbors[B].append(A)
|
||||
def zebra_constraint(A, a, B, b, recurse=0):
|
||||
same = (a == b)
|
||||
next_to = abs(a - b) == 1
|
||||
if A == 'Englishman' and B == 'Red': return same
|
||||
if A == 'Spaniard' and B == 'Dog': return same
|
||||
if A == 'Chesterfields' and B == 'Fox': return next_to
|
||||
if A == 'Norwegian' and B == 'Blue': return next_to
|
||||
if A == 'Kools' and B == 'Yellow': return same
|
||||
if A == 'Winston' and B == 'Snails': return same
|
||||
if A == 'LuckyStrike' and B == 'OJ': return same
|
||||
if A == 'Ukranian' and B == 'Tea': return same
|
||||
if A == 'Japanese' and B == 'Parliaments': return same
|
||||
if A == 'Kools' and B == 'Horse': return next_to
|
||||
if A == 'Coffee' and B == 'Green': return same
|
||||
if A == 'Green' and B == 'Ivory': return (a - 1) == b
|
||||
if recurse == 0: return zebra_constraint(B, b, A, a, 1)
|
||||
if ((A in Colors and B in Colors) or
|
||||
(A in Pets and B in Pets) or
|
||||
(A in Drinks and B in Drinks) or
|
||||
(A in Countries and B in Countries) or
|
||||
(A in Smokes and B in Smokes)): return not same
|
||||
raise 'error'
|
||||
return CSP(vars, domains, neighbors, zebra_constraint)
|
||||
|
||||
def solve_zebra(algorithm=min_conflicts, **args):
|
||||
z = Zebra()
|
||||
ans = algorithm(z, **args)
|
||||
for h in range(1, 6):
|
||||
print 'House', h,
|
||||
for (var, val) in ans.items():
|
||||
if val == h: print var,
|
||||
print
|
||||
return ans['Zebra'], ans['Water'], z.nassigns, ans,
|
||||
|
||||
|
@ -0,0 +1,736 @@
|
||||
"""Search (Chapters 3-4)
|
||||
|
||||
The way to use this code is to subclass Problem to create a class of problems,
|
||||
then create problem instances and solve them with calls to the various search
|
||||
functions."""
|
||||
|
||||
from __future__ import generators
|
||||
from utils import *
|
||||
import agents
|
||||
import math, random, sys, time, bisect, string
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class Problem:
|
||||
"""The abstract class for a formal problem. You should subclass this and
|
||||
implement the method successor, and possibly __init__, goal_test, and
|
||||
path_cost. Then you will create instances of your subclass and solve them
|
||||
with the various search functions."""
|
||||
|
||||
def __init__(self, initial, goal=None):
|
||||
"""The constructor specifies the initial state, and possibly a goal
|
||||
state, if there is a unique goal. Your subclass's constructor can add
|
||||
other arguments."""
|
||||
self.initial = initial; self.goal = goal
|
||||
|
||||
def successor(self, state):
|
||||
"""Given a state, return a sequence of (action, state) pairs reachable
|
||||
from this state. If there are many successors, consider an iterator
|
||||
that yields the successors one at a time, rather than building them
|
||||
all at once. Iterators will work fine within the framework."""
|
||||
abstract
|
||||
|
||||
def goal_test(self, state):
|
||||
"""Return True if the state is a goal. The default method compares the
|
||||
state to self.goal, as specified in the constructor. Implement this
|
||||
method if checking against a single self.goal is not enough."""
|
||||
return state == self.goal
|
||||
|
||||
def path_cost(self, c, state1, action, state2):
|
||||
"""Return the cost of a solution path that arrives at state2 from
|
||||
state1 via action, assuming cost c to get up to state1. If the problem
|
||||
is such that the path doesn't matter, this function will only look at
|
||||
state2. If the path does matter, it will consider c and maybe state1
|
||||
and action. The default method costs 1 for every step in the path."""
|
||||
return c + 1
|
||||
|
||||
def value(self):
|
||||
"""For optimization problems, each state has a value. Hill-climbing
|
||||
and related algorithms try to maximize this value."""
|
||||
abstract
|
||||
#______________________________________________________________________________
|
||||
|
||||
class Node:
|
||||
"""A node in a search tree. Contains a pointer to the parent (the node
|
||||
that this is a successor of) and to the actual state for this node. Note
|
||||
that if a state is arrived at by two paths, then there are two nodes with
|
||||
the same state. Also includes the action that got us to this state, and
|
||||
the total path_cost (also known as g) to reach the node. Other functions
|
||||
may add an f and h value; see best_first_graph_search and astar_search for
|
||||
an explanation of how the f and h values are handled. You will not need to
|
||||
subclass this class."""
|
||||
|
||||
def __init__(self, state, parent=None, action=None, path_cost=0):
|
||||
"Create a search tree Node, derived from a parent by an action."
|
||||
update(self, state=state, parent=parent, action=action,
|
||||
path_cost=path_cost, depth=0)
|
||||
if parent:
|
||||
self.depth = parent.depth + 1
|
||||
|
||||
def __repr__(self):
|
||||
return "<Node %s>" % (self.state,)
|
||||
|
||||
def path(self):
|
||||
"Create a list of nodes from the root to this node."
|
||||
x, result = self, [self]
|
||||
while x.parent:
|
||||
result.append(x.parent)
|
||||
x = x.parent
|
||||
return result
|
||||
|
||||
def expand(self, problem):
|
||||
"Return a list of nodes reachable from this node. [Fig. 3.8]"
|
||||
return [Node(next, self, act,
|
||||
problem.path_cost(self.path_cost, self.state, act, next))
|
||||
for (act, next) in problem.successor(self.state)]
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
class SimpleProblemSolvingAgent(agents.Agent):
|
||||
"""Abstract framework for problem-solving agent. [Fig. 3.1]"""
|
||||
def __init__(self):
|
||||
Agent.__init__(self)
|
||||
state = []
|
||||
seq = []
|
||||
|
||||
def program(percept):
|
||||
state = self.update_state(state, percept)
|
||||
if not seq:
|
||||
goal = self.formulate_goal(state)
|
||||
problem = self.formulate_problem(state, goal)
|
||||
seq = self.search(problem)
|
||||
action = seq[0]
|
||||
seq[0:1] = []
|
||||
return action
|
||||
|
||||
self.program = program
|
||||
|
||||
#______________________________________________________________________________
|
||||
## Uninformed Search algorithms
|
||||
|
||||
def tree_search(problem, fringe):
|
||||
"""Search through the successors of a problem to find a goal.
|
||||
The argument fringe should be an empty queue.
|
||||
Don't worry about repeated paths to a state. [Fig. 3.8]"""
|
||||
fringe.append(Node(problem.initial))
|
||||
while fringe:
|
||||
node = fringe.pop()
|
||||
if problem.goal_test(node.state):
|
||||
return node
|
||||
fringe.extend(node.expand(problem))
|
||||
return None
|
||||
|
||||
def breadth_first_tree_search(problem):
|
||||
"Search the shallowest nodes in the search tree first. [p 74]"
|
||||
return tree_search(problem, FIFOQueue())
|
||||
|
||||
def depth_first_tree_search(problem):
|
||||
"Search the deepest nodes in the search tree first. [p 74]"
|
||||
return tree_search(problem, Stack())
|
||||
|
||||
def graph_search(problem, fringe):
|
||||
"""Search through the successors of a problem to find a goal.
|
||||
The argument fringe should be an empty queue.
|
||||
If two paths reach a state, only use the best one. [Fig. 3.18]"""
|
||||
closed = {}
|
||||
fringe.append(Node(problem.initial))
|
||||
while fringe:
|
||||
node = fringe.pop()
|
||||
if problem.goal_test(node.state):
|
||||
return node
|
||||
if node.state not in closed:
|
||||
closed[node.state] = True
|
||||
fringe.extend(node.expand(problem))
|
||||
return None
|
||||
|
||||
def breadth_first_graph_search(problem):
|
||||
"Search the shallowest nodes in the search tree first. [p 74]"
|
||||
return graph_search(problem, FIFOQueue())
|
||||
|
||||
def depth_first_graph_search(problem):
|
||||
"Search the deepest nodes in the search tree first. [p 74]"
|
||||
return graph_search(problem, Stack())
|
||||
|
||||
def depth_limited_search(problem, limit=50):
|
||||
"[Fig. 3.12]"
|
||||
def recursive_dls(node, problem, limit):
|
||||
cutoff_occurred = False
|
||||
if problem.goal_test(node.state):
|
||||
return node
|
||||
elif node.depth == limit:
|
||||
return 'cutoff'
|
||||
else:
|
||||
for successor in node.expand(problem):
|
||||
result = recursive_dls(successor, problem, limit)
|
||||
if result == 'cutoff':
|
||||
cutoff_occurred = True
|
||||
elif result != None:
|
||||
return result
|
||||
if cutoff_occurred:
|
||||
return 'cutoff'
|
||||
else:
|
||||
return None
|
||||
# Body of depth_limited_search:
|
||||
return recursive_dls(Node(problem.initial), problem, limit)
|
||||
|
||||
def iterative_deepening_search(problem):
|
||||
"[Fig. 3.13]"
|
||||
for depth in xrange(sys.maxint):
|
||||
result = depth_limited_search(problem, depth)
|
||||
if result is not 'cutoff':
|
||||
return result
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Informed (Heuristic) Search
|
||||
|
||||
def best_first_graph_search(problem, f):
|
||||
"""Search the nodes with the lowest f scores first.
|
||||
You specify the function f(node) that you want to minimize; for example,
|
||||
if f is a heuristic estimate to the goal, then we have greedy best
|
||||
first search; if f is node.depth then we have depth-first search.
|
||||
There is a subtlety: the line "f = memoize(f, 'f')" means that the f
|
||||
values will be cached on the nodes as they are computed. So after doing
|
||||
a best first search you can examine the f values of the path returned."""
|
||||
f = memoize(f, 'f')
|
||||
return graph_search(problem, PriorityQueue(min, f))
|
||||
|
||||
greedy_best_first_graph_search = best_first_graph_search
|
||||
# Greedy best-first search is accomplished by specifying f(n) = h(n).
|
||||
|
||||
def astar_search(problem, h=None):
|
||||
"""A* search is best-first graph search with f(n) = g(n)+h(n).
|
||||
You need to specify the h function when you call astar_search.
|
||||
Uses the pathmax trick: f(n) = max(f(n), g(n)+h(n))."""
|
||||
h = h or problem.h
|
||||
def f(n):
|
||||
return max(getattr(n, 'f', -infinity), n.path_cost + h(n))
|
||||
return best_first_graph_search(problem, f)
|
||||
|
||||
#______________________________________________________________________________
|
||||
## Other search algorithms
|
||||
|
||||
def recursive_best_first_search(problem):
|
||||
"[Fig. 4.5]"
|
||||
def RBFS(problem, node, flimit):
|
||||
if problem.goal_test(node.state):
|
||||
return node
|
||||
successors = expand(node, problem)
|
||||
if len(successors) == 0:
|
||||
return None, infinity
|
||||
for s in successors:
|
||||
s.f = max(s.path_cost + s.h, node.f)
|
||||
while True:
|
||||
successors.sort(lambda x,y: x.f - y.f) # Order by lowest f value
|
||||
best = successors[0]
|
||||
if best.f > flimit:
|
||||
return None, best.f
|
||||
alternative = successors[1]
|
||||
result, best.f = RBFS(problem, best, min(flimit, alternative))
|
||||
if result is not None:
|
||||
return result
|
||||
return RBFS(Node(problem.initial), infinity)
|
||||
|
||||
|
||||
def hill_climbing(problem):
|
||||
"""From the initial node, keep choosing the neighbor with highest value,
|
||||
stopping when no neighbor is better. [Fig. 4.11]"""
|
||||
current = Node(problem.initial)
|
||||
while True:
|
||||
neighbor = argmax(expand(node, problem), Node.value)
|
||||
if neighbor.value() <= current.value():
|
||||
return current.state
|
||||
current = neighbor
|
||||
|
||||
def exp_schedule(k=20, lam=0.005, limit=100):
|
||||
"One possible schedule function for simulated annealing"
|
||||
return lambda t: if_(t < limit, k * math.exp(-lam * t), 0)
|
||||
|
||||
def simulated_annealing(problem, schedule=exp_schedule()):
|
||||
"[Fig. 4.5]"
|
||||
current = Node(problem.initial)
|
||||
for t in xrange(sys.maxint):
|
||||
T = schedule(t)
|
||||
if T == 0:
|
||||
return current
|
||||
next = random.choice(expand(node. problem))
|
||||
delta_e = next.path_cost - current.path_cost
|
||||
if delta_e > 0 or probability(math.exp(delta_e/T)):
|
||||
current = next
|
||||
|
||||
def online_dfs_agent(a):
|
||||
"[Fig. 4.12]"
|
||||
pass #### more
|
||||
|
||||
def lrta_star_agent(a):
|
||||
"[Fig. 4.12]"
|
||||
pass #### more
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Genetic Algorithm
|
||||
|
||||
def genetic_search(problem, fitness_fn, ngen=1000, pmut=0.0, n=20):
|
||||
"""Call genetic_algorithm on the appropriate parts of a problem.
|
||||
This requires that the problem has a successor function that generates
|
||||
reasonable states, and that it has a path_cost function that scores states.
|
||||
We use the negative of the path_cost function, because costs are to be
|
||||
minimized, while genetic-algorithm expects a fitness_fn to be maximized."""
|
||||
states = [s for (a, s) in problem.successor(problem.initial_state)[:n]]
|
||||
random.shuffle(states)
|
||||
fitness_fn = lambda s: - problem.path_cost(0, s, None, s)
|
||||
return genetic_algorithm(states, fitness_fn, ngen, pmut)
|
||||
|
||||
def genetic_algorithm(population, fitness_fn, ngen=1000, pmut=0.0):
|
||||
"""[Fig. 4.7]"""
|
||||
def reproduce(p1, p2):
|
||||
c = random.randrange(len(p1))
|
||||
return p1[:c] + p2[c:]
|
||||
|
||||
for i in range(ngen):
|
||||
new_population = []
|
||||
for i in len(population):
|
||||
p1, p2 = random_weighted_selections(population, 2, fitness_fn)
|
||||
child = reproduce(p1, p2)
|
||||
if random.uniform(0,1) > pmut:
|
||||
child.mutate()
|
||||
new_population.append(child)
|
||||
population = new_population
|
||||
return argmax(population, fitness_fn)
|
||||
|
||||
def random_weighted_selection(seq, n, weight_fn):
|
||||
"""Pick n elements of seq, weighted according to weight_fn.
|
||||
That is, apply weight_fn to each element of seq, add up the total.
|
||||
Then choose an element e with probability weight[e]/total.
|
||||
Repeat n times, with replacement. """
|
||||
totals = []; runningtotal = 0
|
||||
for item in seq:
|
||||
runningtotal += weight_fn(item)
|
||||
totals.append(runningtotal)
|
||||
selections = []
|
||||
for s in range(n):
|
||||
r = random.uniform(0, totals[-1])
|
||||
for i in range(len(seq)):
|
||||
if totals[i] > r:
|
||||
selections.append(seq[i])
|
||||
break
|
||||
return selections
|
||||
|
||||
|
||||
#_____________________________________________________________________________
|
||||
# The remainder of this file implements examples for the search algorithms.
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Graphs and Graph Problems
|
||||
|
||||
class Graph:
|
||||
"""A graph connects nodes (verticies) by edges (links). Each edge can also
|
||||
have a length associated with it. The constructor call is something like:
|
||||
g = Graph({'A': {'B': 1, 'C': 2})
|
||||
this makes a graph with 3 nodes, A, B, and C, with an edge of length 1 from
|
||||
A to B, and an edge of length 2 from A to C. You can also do:
|
||||
g = Graph({'A': {'B': 1, 'C': 2}, directed=False)
|
||||
This makes an undirected graph, so inverse links are also added. The graph
|
||||
stays undirected; if you add more links with g.connect('B', 'C', 3), then
|
||||
inverse link is also added. You can use g.nodes() to get a list of nodes,
|
||||
g.get('A') to get a dict of links out of A, and g.get('A', 'B') to get the
|
||||
length of the link from A to B. 'Lengths' can actually be any object at
|
||||
all, and nodes can be any hashable object."""
|
||||
|
||||
def __init__(self, dict=None, directed=True):
|
||||
self.dict = dict or {}
|
||||
self.directed = directed
|
||||
if not directed: self.make_undirected()
|
||||
|
||||
def make_undirected(self):
|
||||
"Make a digraph into an undirected graph by adding symmetric edges."
|
||||
for a in self.dict.keys():
|
||||
for (b, distance) in self.dict[a].items():
|
||||
self.connect1(b, a, distance)
|
||||
|
||||
def connect(self, A, B, distance=1):
|
||||
"""Add a link from A and B of given distance, and also add the inverse
|
||||
link if the graph is undirected."""
|
||||
self.connect1(A, B, distance)
|
||||
if not self.directed: self.connect1(B, A, distance)
|
||||
|
||||
def connect1(self, A, B, distance):
|
||||
"Add a link from A to B of given distance, in one direction only."
|
||||
self.dict.setdefault(A,{})[B] = distance
|
||||
|
||||
def get(self, a, b=None):
|
||||
"""Return a link distance or a dict of {node: distance} entries.
|
||||
.get(a,b) returns the distance or None;
|
||||
.get(a) returns a dict of {node: distance} entries, possibly {}."""
|
||||
links = self.dict.setdefault(a, {})
|
||||
if b is None: return links
|
||||
else: return links.get(b)
|
||||
|
||||
def nodes(self):
|
||||
"Return a list of nodes in the graph."
|
||||
return self.dict.keys()
|
||||
|
||||
def UndirectedGraph(dict=None):
|
||||
"Build a Graph where every edge (including future ones) goes both ways."
|
||||
return Graph(dict=dict, directed=False)
|
||||
|
||||
def RandomGraph(nodes=range(10), min_links=2, width=400, height=300,
|
||||
curvature=lambda: random.uniform(1.1, 1.5)):
|
||||
"""Construct a random graph, with the specified nodes, and random links.
|
||||
The nodes are laid out randomly on a (width x height) rectangle.
|
||||
Then each node is connected to the min_links nearest neighbors.
|
||||
Because inverse links are added, some nodes will have more connections.
|
||||
The distance between nodes is the hypotenuse times curvature(),
|
||||
where curvature() defaults to a random number between 1.1 and 1.5."""
|
||||
g = UndirectedGraph()
|
||||
g.locations = {}
|
||||
## Build the cities
|
||||
for node in nodes:
|
||||
g.locations[node] = (random.randrange(width), random.randrange(height))
|
||||
## Build roads from each city to at least min_links nearest neighbors.
|
||||
for i in range(min_links):
|
||||
for node in nodes:
|
||||
if len(g.get(node)) < min_links:
|
||||
here = g.locations[node]
|
||||
def distance_to_node(n):
|
||||
if n is node or g.get(node,n): return infinity
|
||||
return distance(g.locations[n], here)
|
||||
neighbor = argmin(nodes, distance_to_node)
|
||||
d = distance(g.locations[neighbor], here) * curvature()
|
||||
g.connect(node, neighbor, int(d))
|
||||
return g
|
||||
|
||||
romania = UndirectedGraph(Dict(
|
||||
A=Dict(Z=75, S=140, T=118),
|
||||
B=Dict(U=85, P=101, G=90, F=211),
|
||||
C=Dict(D=120, R=146, P=138),
|
||||
D=Dict(M=75),
|
||||
E=Dict(H=86),
|
||||
F=Dict(S=99),
|
||||
H=Dict(U=98),
|
||||
I=Dict(V=92, N=87),
|
||||
L=Dict(T=111, M=70),
|
||||
O=Dict(Z=71, S=151),
|
||||
P=Dict(R=97),
|
||||
R=Dict(S=80),
|
||||
U=Dict(V=142)))
|
||||
romania.locations = Dict(
|
||||
A=( 91, 492), B=(400, 327), C=(253, 288), D=(165, 299),
|
||||
E=(562, 293), F=(305, 449), G=(375, 270), H=(534, 350),
|
||||
I=(473, 506), L=(165, 379), M=(168, 339), N=(406, 537),
|
||||
O=(131, 571), P=(320, 368), R=(233, 410), S=(207, 457),
|
||||
T=( 94, 410), U=(456, 350), V=(509, 444), Z=(108, 531))
|
||||
|
||||
australia = UndirectedGraph(Dict(
|
||||
T=Dict(),
|
||||
SA=Dict(WA=1, NT=1, Q=1, NSW=1, V=1),
|
||||
NT=Dict(WA=1, Q=1),
|
||||
NSW=Dict(Q=1, V=1)))
|
||||
australia.locations = Dict(WA=(120, 24), NT=(135, 20), SA=(135, 30),
|
||||
Q=(145, 20), NSW=(145, 32), T=(145, 42), V=(145, 37))
|
||||
|
||||
class GraphProblem(Problem):
|
||||
"The problem of searching a graph from one node to another."
|
||||
def __init__(self, initial, goal, graph):
|
||||
Problem.__init__(self, initial, goal)
|
||||
self.graph = graph
|
||||
|
||||
def successor(self, A):
|
||||
"Return a list of (action, result) pairs."
|
||||
return [(B, B) for B in self.graph.get(A).keys()]
|
||||
|
||||
def path_cost(self, cost_so_far, A, action, B):
|
||||
return cost_so_far + (self.graph.get(A,B) or infinity)
|
||||
|
||||
def h(self, node):
|
||||
"h function is straight-line distance from a node's state to goal."
|
||||
locs = getattr(self.graph, 'locations', None)
|
||||
if locs:
|
||||
return int(distance(locs[node.state], locs[self.goal]))
|
||||
else:
|
||||
return infinity
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
#### NOTE: NQueensProblem not working properly yet.
|
||||
|
||||
class NQueensProblem(Problem):
|
||||
"""The problem of placing N queens on an NxN board with none attacking
|
||||
each other. A state is represented as an N-element array, where the
|
||||
a value of r in the c-th entry means there is a queen at column c,
|
||||
row r, and a value of None means that the c-th column has not been
|
||||
filled in left. We fill in columns left to right."""
|
||||
def __init__(self, N):
|
||||
self.N = N
|
||||
self.initial = [None] * N
|
||||
|
||||
def successor(self, state):
|
||||
"In the leftmost empty column, try all non-conflicting rows."
|
||||
if state[-1] is not None:
|
||||
return [] ## All columns filled; no successors
|
||||
else:
|
||||
def place(col, row):
|
||||
new = state[:]
|
||||
new[col] = row
|
||||
return new
|
||||
col = state.index(None)
|
||||
return [(row, place(col, row)) for row in range(self.N)
|
||||
if not self.conflicted(state, row, col)]
|
||||
|
||||
def conflicted(self, state, row, col):
|
||||
"Would placing a queen at (row, col) conflict with anything?"
|
||||
for c in range(col-1):
|
||||
if self.conflict(row, col, state[c], c):
|
||||
return True
|
||||
return False
|
||||
|
||||
def conflict(self, row1, col1, row2, col2):
|
||||
"Would putting two queens in (row1, col1) and (row2, col2) conflict?"
|
||||
return (row1 == row2 ## same row
|
||||
or col1 == col2 ## same column
|
||||
or row1-col1 == row2-col2 ## same \ diagonal
|
||||
or row1+col1 == row2+col2) ## same / diagonal
|
||||
|
||||
def goal_test(self, state):
|
||||
"Check if all columns filled, no conflicts."
|
||||
if state[-1] is None:
|
||||
return False
|
||||
for c in range(len(state)):
|
||||
if self.conflicted(state, state[c], c):
|
||||
return False
|
||||
return True
|
||||
|
||||
#______________________________________________________________________________
|
||||
## Inverse Boggle: Search for a high-scoring Boggle board. A good domain for
|
||||
## iterative-repair and related search tehniques, as suggested by Justin Boyan.
|
||||
|
||||
ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
|
||||
cubes16 = ['FORIXB', 'MOQABJ', 'GURILW', 'SETUPL',
|
||||
'CMPDAE', 'ACITAO', 'SLCRAE', 'ROMASH',
|
||||
'NODESW', 'HEFIYE', 'ONUDTK', 'TEVIGN',
|
||||
'ANEDVZ', 'PINESH', 'ABILYT', 'GKYLEU']
|
||||
|
||||
def random_boggle(n=4):
|
||||
"""Return a random Boggle board of size n x n.
|
||||
We represent a board as a linear list of letters."""
|
||||
cubes = [cubes16[i % 16] for i in range(n*n)]
|
||||
random.shuffle(cubes)
|
||||
return map(random.choice, cubes)
|
||||
|
||||
## The best 5x5 board found by Boyan, with our word list this board scores
|
||||
## 2274 words, for a score of 9837
|
||||
|
||||
boyan_best = list('RSTCSDEIAEGNLRPEATESMSSID')
|
||||
|
||||
def print_boggle(board):
|
||||
"Print the board in a 2-d array."
|
||||
n2 = len(board); n = exact_sqrt(n2)
|
||||
for i in range(n2):
|
||||
if i % n == 0: print
|
||||
if board[i] == 'Q': print 'Qu',
|
||||
else: print str(board[i]) + ' ',
|
||||
print
|
||||
|
||||
def boggle_neighbors(n2, cache={}):
|
||||
""""Return a list of lists, where the i-th element is the list of indexes
|
||||
for the neighbors of square i."""
|
||||
if cache.get(n2):
|
||||
return cache.get(n2)
|
||||
n = exact_sqrt(n2)
|
||||
neighbors = [None] * n2
|
||||
for i in range(n2):
|
||||
neighbors[i] = []
|
||||
on_top = i < n
|
||||
on_bottom = i >= n2 - n
|
||||
on_left = i % n == 0
|
||||
on_right = (i+1) % n == 0
|
||||
if not on_top:
|
||||
neighbors[i].append(i - n)
|
||||
if not on_left: neighbors[i].append(i - n - 1)
|
||||
if not on_right: neighbors[i].append(i - n + 1)
|
||||
if not on_bottom:
|
||||
neighbors[i].append(i + n)
|
||||
if not on_left: neighbors[i].append(i + n - 1)
|
||||
if not on_right: neighbors[i].append(i + n + 1)
|
||||
if not on_left: neighbors[i].append(i - 1)
|
||||
if not on_right: neighbors[i].append(i + 1)
|
||||
cache[n2] = neighbors
|
||||
return neighbors
|
||||
|
||||
def exact_sqrt(n2):
|
||||
"If n2 is a perfect square, return its square root, else raise error."
|
||||
n = int(math.sqrt(n2))
|
||||
assert n * n == n2
|
||||
return n
|
||||
|
||||
##_____________________________________________________________________________
|
||||
|
||||
class Wordlist:
|
||||
"""This class holds a list of words. You can use (word in wordlist)
|
||||
to check if a word is in the list, or wordlist.lookup(prefix)
|
||||
to see if prefix starts any of the words in the list."""
|
||||
def __init__(self, filename, min_len=3):
|
||||
lines = open(filename).read().upper().split()
|
||||
self.words = [word for word in lines if len(word) >= min_len]
|
||||
self.words.sort()
|
||||
self.bounds = {}
|
||||
for c in ALPHABET:
|
||||
c2 = chr(ord(c) + 1)
|
||||
self.bounds[c] = (bisect.bisect(self.words, c),
|
||||
bisect.bisect(self.words, c2))
|
||||
|
||||
def lookup(self, prefix, lo=0, hi=None):
|
||||
"""See if prefix is in dictionary, as a full word or as a prefix.
|
||||
Return two values: the first is the lowest i such that
|
||||
words[i].startswith(prefix), or is None; the second is
|
||||
True iff prefix itself is in the Wordlist."""
|
||||
words = self.words
|
||||
i = bisect.bisect_left(words, prefix, lo, hi)
|
||||
if i < len(words) and words[i].startswith(prefix):
|
||||
return i, (words[i] == prefix)
|
||||
else:
|
||||
return None, False
|
||||
|
||||
def __contains__(self, word):
|
||||
return self.words[bisect.bisect_left(self.words, word)] == word
|
||||
|
||||
def __len__(self):
|
||||
return len(self.words)
|
||||
|
||||
##_____________________________________________________________________________
|
||||
|
||||
class BoggleFinder:
|
||||
"""A class that allows you to find all the words in a Boggle board. """
|
||||
|
||||
wordlist = None ## A class variable, holding a wordlist
|
||||
|
||||
def __init__(self, board=None):
|
||||
if BoggleFinder.wordlist is None:
|
||||
BoggleFinder.wordlist = Wordlist("../data/wordlist")
|
||||
self.found = {}
|
||||
if board:
|
||||
self.set_board(board)
|
||||
|
||||
def set_board(self, board=None):
|
||||
"Set the board, and find all the words in it."
|
||||
if board is None:
|
||||
board = random_boggle()
|
||||
self.board = board
|
||||
self.neighbors = boggle_neighbors(len(board))
|
||||
self.found = {}
|
||||
for i in range(len(board)):
|
||||
lo, hi = self.wordlist.bounds[board[i]]
|
||||
self.find(lo, hi, i, [], '')
|
||||
return self
|
||||
|
||||
def find(self, lo, hi, i, visited, prefix):
|
||||
"""Looking in square i, find the words that continue the prefix,
|
||||
considering the entries in self.wordlist.words[lo:hi], and not
|
||||
revisiting the squares in visited."""
|
||||
if i in visited:
|
||||
return
|
||||
wordpos, is_word = self.wordlist.lookup(prefix, lo, hi)
|
||||
if wordpos is not None:
|
||||
if is_word:
|
||||
self.found[prefix] = True
|
||||
visited.append(i)
|
||||
c = self.board[i]
|
||||
if c == 'Q': c = 'QU'
|
||||
prefix += c
|
||||
for j in self.neighbors[i]:
|
||||
self.find(wordpos, hi, j, visited, prefix)
|
||||
visited.pop()
|
||||
|
||||
def words(self):
|
||||
"The words found."
|
||||
return self.found.keys()
|
||||
|
||||
scores = [0, 0, 0, 0, 1, 2, 3, 5] + [11] * 100
|
||||
|
||||
def score(self):
|
||||
"The total score for the words found, according to the rules."
|
||||
return sum([self.scores[len(w)] for w in self.words()])
|
||||
|
||||
def __len__(self):
|
||||
"The number of words found."
|
||||
return len(self.found)
|
||||
|
||||
##_____________________________________________________________________________
|
||||
|
||||
def boggle_hill_climbing(board=None, ntimes=100, print_it=True):
|
||||
"""Solve inverse Boggle by hill-climbing: find a high-scoring board by
|
||||
starting with a random one and changing it."""
|
||||
finder = BoggleFinder()
|
||||
if board is None:
|
||||
board = random_boggle()
|
||||
best = len(finder.set_board(board))
|
||||
for _ in range(ntimes):
|
||||
i, oldc = mutate_boggle(board)
|
||||
new = len(finder.set_board(board))
|
||||
if new > best:
|
||||
best = new
|
||||
print best, _, board
|
||||
else:
|
||||
board[i] = oldc ## Change back
|
||||
if print_it:
|
||||
print_boggle(board)
|
||||
return board, best
|
||||
|
||||
def mutate_boggle(board):
|
||||
i = random.randrange(len(board))
|
||||
oldc = board[i]
|
||||
board[i] = random.choice(random.choice(cubes16)) ##random.choice(boyan_best)
|
||||
return i, oldc
|
||||
|
||||
#______________________________________________________________________________
|
||||
|
||||
## Code to compare searchers on various problems.
|
||||
|
||||
class InstrumentedProblem(Problem):
|
||||
"""Delegates to a problem, and keeps statistics."""
|
||||
|
||||
def __init__(self, problem):
|
||||
self.problem = problem
|
||||
self.succs = self.goal_tests = self.states = 0
|
||||
self.found = None
|
||||
|
||||
def successor(self, state):
|
||||
"Return a list of (action, state) pairs reachable from this state."
|
||||
result = self.problem.successor(state)
|
||||
self.succs += 1; self.states += len(result)
|
||||
return result
|
||||
|
||||
def goal_test(self, state):
|
||||
"Return true if the state is a goal."
|
||||
self.goal_tests += 1
|
||||
result = self.problem.goal_test(state)
|
||||
if result:
|
||||
self.found = state
|
||||
return result
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr in ('succs', 'goal_tests', 'states'):
|
||||
return self.__dict__[attr]
|
||||
else:
|
||||
return getattr(self.problem, attr)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%4d/%4d/%4d/%s>' % (self.succs, self.goal_tests,
|
||||
self.states, str(self.found)[0:4])
|
||||
|
||||
def compare_searchers(problems, header, searchers=[breadth_first_tree_search,
|
||||
breadth_first_graph_search, depth_first_graph_search,
|
||||
iterative_deepening_search, depth_limited_search,
|
||||
astar_search]):
|
||||
def do(searcher, problem):
|
||||
p = InstrumentedProblem(problem)
|
||||
searcher(p)
|
||||
return p
|
||||
table = [[name(s)] + [do(s, p) for p in problems] for s in searchers]
|
||||
print_table(table, header)
|
||||
|
||||
def compare_graph_searchers():
|
||||
compare_searchers(problems=[GraphProblem('A', 'B', romania),
|
||||
GraphProblem('O', 'N', romania),
|
||||
GraphProblem('Q', 'WA', australia)],
|
||||
header=['Searcher', 'Romania(A,B)', 'Romania(O, N)', 'Australia'])
|
||||
|
@ -0,0 +1,714 @@
|
||||
"""Provide some widely useful utilities. Safe for "from utils import *".
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import generators
|
||||
import operator, math, random, copy, sys, os.path, bisect
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Compatibility with Python 2.2 and 2.3
|
||||
|
||||
# The AIMA code is designed to run in Python 2.2 and up (at some point,
|
||||
# support for 2.2 may go away; 2.2 was released in 2001, and so is over
|
||||
# 3 years old). The first part of this file brings you up to 2.4
|
||||
# compatibility if you are running in Python 2.2 or 2.3:
|
||||
|
||||
try: bool, True, False ## Introduced in 2.3
|
||||
except NameError:
|
||||
class bool(int):
|
||||
"Simple implementation of Booleans, as in PEP 285"
|
||||
def __init__(self, val): self.val = val
|
||||
def __int__(self): return self.val
|
||||
def __repr__(self): return ('False', 'True')[self.val]
|
||||
|
||||
True, False = bool(1), bool(0)
|
||||
|
||||
try: sum ## Introduced in 2.3
|
||||
except NameError:
|
||||
def sum(seq, start=0):
|
||||
"""Sum the elements of seq.
|
||||
>>> sum([1, 2, 3])
|
||||
6
|
||||
"""
|
||||
return reduce(operator.add, seq, start)
|
||||
|
||||
try: enumerate ## Introduced in 2.3
|
||||
except NameError:
|
||||
def enumerate(collection):
|
||||
"""Return an iterator that enumerates pairs of (i, c[i]). PEP 279.
|
||||
>>> list(enumerate('abc'))
|
||||
[(0, 'a'), (1, 'b'), (2, 'c')]
|
||||
"""
|
||||
## Copied from PEP 279
|
||||
i = 0
|
||||
it = iter(collection)
|
||||
while 1:
|
||||
yield (i, it.next())
|
||||
i += 1
|
||||
|
||||
|
||||
try: reversed ## Introduced in 2.4
|
||||
except NameError:
|
||||
def reversed(seq):
|
||||
"""Iterate over x in reverse order.
|
||||
>>> list(reversed([1,2,3]))
|
||||
[3, 2, 1]
|
||||
"""
|
||||
if hasattr(seq, 'keys'):
|
||||
raise ValueError("mappings do not support reverse iteration")
|
||||
i = len(seq)
|
||||
while i > 0:
|
||||
i -= 1
|
||||
yield seq[i]
|
||||
|
||||
|
||||
try: sorted ## Introduced in 2.4
|
||||
except NameError:
|
||||
def sorted(seq, cmp=None, key=None, reverse=False):
|
||||
"""Copy seq and sort and return it.
|
||||
>>> sorted([3, 1, 2])
|
||||
[1, 2, 3]
|
||||
"""
|
||||
seq2 = copy.copy(seq)
|
||||
if key:
|
||||
if cmp == None:
|
||||
cmp = __builtins__.cmp
|
||||
seq2.sort(lambda x,y: cmp(key(x), key(y)))
|
||||
else:
|
||||
if cmp == None:
|
||||
seq2.sort()
|
||||
else:
|
||||
seq2.sort(cmp)
|
||||
if reverse:
|
||||
seq2.reverse()
|
||||
return seq2
|
||||
|
||||
try:
|
||||
set, frozenset ## set builtin introduced in 2.4
|
||||
except NameError:
|
||||
try:
|
||||
import sets ## sets module introduced in 2.3
|
||||
set, frozenset = sets.Set, sets.ImmutableSet
|
||||
except (NameError, ImportError):
|
||||
class BaseSet:
|
||||
"set type (see http://docs.python.org/lib/types-set.html)"
|
||||
|
||||
|
||||
def __init__(self, elements=[]):
|
||||
self.dict = {}
|
||||
for e in elements:
|
||||
self.dict[e] = 1
|
||||
|
||||
def __len__(self):
|
||||
return len(self.dict)
|
||||
|
||||
def __iter__(self):
|
||||
for e in self.dict:
|
||||
yield e
|
||||
|
||||
def __contains__(self, element):
|
||||
return element in self.dict
|
||||
|
||||
def issubset(self, other):
|
||||
for e in self.dict.keys():
|
||||
if e not in other:
|
||||
return False
|
||||
return True
|
||||
|
||||
def issuperset(self, other):
|
||||
for e in other:
|
||||
if e not in self:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def union(self, other):
|
||||
return type(self)(list(self) + list(other))
|
||||
|
||||
def intersection(self, other):
|
||||
return type(self)([e for e in self.dict if e in other])
|
||||
|
||||
def difference(self, other):
|
||||
return type(self)([e for e in self.dict if e not in other])
|
||||
|
||||
def symmetric_difference(self, other):
|
||||
return type(self)([e for e in self.dict if e not in other] +
|
||||
[e for e in other if e not in self.dict])
|
||||
|
||||
def copy(self):
|
||||
return type(self)(self.dict)
|
||||
|
||||
def __repr__(self):
|
||||
elements = ", ".join(map(str, self.dict))
|
||||
return "%s([%s])" % (type(self).__name__, elements)
|
||||
|
||||
__le__ = issubset
|
||||
__ge__ = issuperset
|
||||
__or__ = union
|
||||
__and__ = intersection
|
||||
__sub__ = difference
|
||||
__xor__ = symmetric_difference
|
||||
|
||||
class frozenset(BaseSet):
|
||||
"A frozenset is a BaseSet that has a hash value and is immutable."
|
||||
|
||||
def __init__(self, elements=[]):
|
||||
BaseSet.__init__(elements)
|
||||
self.hash = 0
|
||||
for e in self:
|
||||
self.hash |= hash(e)
|
||||
|
||||
def __hash__(self):
|
||||
return self.hash
|
||||
|
||||
class set(BaseSet):
|
||||
"A set is a BaseSet that does not have a hash, but is mutable."
|
||||
|
||||
def update(self, other):
|
||||
for e in other:
|
||||
self.add(e)
|
||||
return self
|
||||
|
||||
def intersection_update(self, other):
|
||||
for e in self.dict.keys():
|
||||
if e not in other:
|
||||
self.remove(e)
|
||||
return self
|
||||
|
||||
def difference_update(self, other):
|
||||
for e in self.dict.keys():
|
||||
if e in other:
|
||||
self.remove(e)
|
||||
return self
|
||||
|
||||
def symmetric_difference_update(self, other):
|
||||
to_remove1 = [e for e in self.dict if e in other]
|
||||
to_remove2 = [e for e in other if e in self.dict]
|
||||
self.difference_update(to_remove1)
|
||||
self.difference_update(to_remove2)
|
||||
return self
|
||||
|
||||
def add(self, element):
|
||||
self.dict[element] = 1
|
||||
|
||||
def remove(self, element):
|
||||
del self.dict[element]
|
||||
|
||||
def discard(self, element):
|
||||
if element in self.dict:
|
||||
del self.dict[element]
|
||||
|
||||
def pop(self):
|
||||
key, val = self.dict.popitem()
|
||||
return key
|
||||
|
||||
def clear(self):
|
||||
self.dict.clear()
|
||||
|
||||
__ior__ = update
|
||||
__iand__ = intersection_update
|
||||
__isub__ = difference_update
|
||||
__ixor__ = symmetric_difference_update
|
||||
|
||||
|
||||
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Simple Data Structures: infinity, Dict, Struct
|
||||
|
||||
infinity = 1.0e400
|
||||
|
||||
def Dict(**entries):
|
||||
"""Create a dict out of the argument=value arguments.
|
||||
>>> Dict(a=1, b=2, c=3)
|
||||
{'a': 1, 'c': 3, 'b': 2}
|
||||
"""
|
||||
return entries
|
||||
|
||||
class DefaultDict(dict):
|
||||
"""Dictionary with a default value for unknown keys."""
|
||||
def __init__(self, default):
|
||||
self.default = default
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self: return self.get(key)
|
||||
return self.setdefault(key, copy.deepcopy(self.default))
|
||||
|
||||
def __copy__(self):
|
||||
copy = DefaultDict(self.default)
|
||||
copy.update(self)
|
||||
return copy
|
||||
|
||||
class Struct:
|
||||
"""Create an instance with argument=value slots.
|
||||
This is for making a lightweight object whose class doesn't matter."""
|
||||
def __init__(self, **entries):
|
||||
self.__dict__.update(entries)
|
||||
|
||||
def __cmp__(self, other):
|
||||
if isinstance(other, Struct):
|
||||
return cmp(self.__dict__, other.__dict__)
|
||||
else:
|
||||
return cmp(self.__dict__, other)
|
||||
|
||||
def __repr__(self):
|
||||
args = ['%s=%s' % (k, repr(v)) for (k, v) in vars(self).items()]
|
||||
return 'Struct(%s)' % ', '.join(args)
|
||||
|
||||
def update(x, **entries):
|
||||
"""Update a dict; or an object with slots; according to entries.
|
||||
>>> update({'a': 1}, a=10, b=20)
|
||||
{'a': 10, 'b': 20}
|
||||
>>> update(Struct(a=1), a=10, b=20)
|
||||
Struct(a=10, b=20)
|
||||
"""
|
||||
if isinstance(x, dict):
|
||||
x.update(entries)
|
||||
else:
|
||||
x.__dict__.update(entries)
|
||||
return x
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Functions on Sequences (mostly inspired by Common Lisp)
|
||||
# NOTE: Sequence functions (count_if, find_if, every, some) take function
|
||||
# argument first (like reduce, filter, and map).
|
||||
|
||||
def removeall(item, seq):
|
||||
"""Return a copy of seq (or string) with all occurences of item removed.
|
||||
>>> removeall(3, [1, 2, 3, 3, 2, 1, 3])
|
||||
[1, 2, 2, 1]
|
||||
>>> removeall(4, [1, 2, 3])
|
||||
[1, 2, 3]
|
||||
"""
|
||||
if isinstance(seq, str):
|
||||
return seq.replace(item, '')
|
||||
else:
|
||||
return [x for x in seq if x != item]
|
||||
|
||||
def unique(seq):
|
||||
"""Remove duplicate elements from seq. Assumes hashable elements.
|
||||
>>> unique([1, 2, 3, 2, 1])
|
||||
[1, 2, 3]
|
||||
"""
|
||||
return list(set(seq))
|
||||
|
||||
def product(numbers):
|
||||
"""Return the product of the numbers.
|
||||
>>> product([1,2,3,4])
|
||||
24
|
||||
"""
|
||||
return reduce(operator.mul, numbers, 1)
|
||||
|
||||
def count_if(predicate, seq):
|
||||
"""Count the number of elements of seq for which the predicate is true.
|
||||
>>> count_if(callable, [42, None, max, min])
|
||||
2
|
||||
"""
|
||||
f = lambda count, x: count + (not not predicate(x))
|
||||
return reduce(f, seq, 0)
|
||||
|
||||
def find_if(predicate, seq):
|
||||
"""If there is an element of seq that satisfies predicate; return it.
|
||||
>>> find_if(callable, [3, min, max])
|
||||
<built-in function min>
|
||||
>>> find_if(callable, [1, 2, 3])
|
||||
"""
|
||||
for x in seq:
|
||||
if predicate(x): return x
|
||||
return None
|
||||
|
||||
def every(predicate, seq):
|
||||
"""True if every element of seq satisfies predicate.
|
||||
>>> every(callable, [min, max])
|
||||
1
|
||||
>>> every(callable, [min, 3])
|
||||
0
|
||||
"""
|
||||
for x in seq:
|
||||
if not predicate(x): return False
|
||||
return True
|
||||
|
||||
def some(predicate, seq):
|
||||
"""If some element x of seq satisfies predicate(x), return predicate(x).
|
||||
>>> some(callable, [min, 3])
|
||||
1
|
||||
>>> some(callable, [2, 3])
|
||||
0
|
||||
"""
|
||||
for x in seq:
|
||||
px = predicate(x)
|
||||
if px: return px
|
||||
return False
|
||||
|
||||
def isin(elt, seq):
|
||||
"""Like (elt in seq), but compares with is, not ==.
|
||||
>>> e = []; isin(e, [1, e, 3])
|
||||
True
|
||||
>>> isin(e, [1, [], 3])
|
||||
False
|
||||
"""
|
||||
for x in seq:
|
||||
if elt is x: return True
|
||||
return False
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Functions on sequences of numbers
|
||||
# NOTE: these take the sequence argument first, like min and max,
|
||||
# and like standard math notation: \sigma (i = 1..n) fn(i)
|
||||
# A lot of programing is finding the best value that satisfies some condition;
|
||||
# so there are three versions of argmin/argmax, depending on what you want to
|
||||
# do with ties: return the first one, return them all, or pick at random.
|
||||
|
||||
|
||||
def argmin(seq, fn):
|
||||
"""Return an element with lowest fn(seq[i]) score; tie goes to first one.
|
||||
>>> argmin(['one', 'to', 'three'], len)
|
||||
'to'
|
||||
"""
|
||||
best = seq[0]; best_score = fn(best)
|
||||
for x in seq:
|
||||
x_score = fn(x)
|
||||
if x_score < best_score:
|
||||
best, best_score = x, x_score
|
||||
return best
|
||||
|
||||
def argmin_list(seq, fn):
|
||||
"""Return a list of elements of seq[i] with the lowest fn(seq[i]) scores.
|
||||
>>> argmin_list(['one', 'to', 'three', 'or'], len)
|
||||
['to', 'or']
|
||||
"""
|
||||
best_score, best = fn(seq[0]), []
|
||||
for x in seq:
|
||||
x_score = fn(x)
|
||||
if x_score < best_score:
|
||||
best, best_score = [x], x_score
|
||||
elif x_score == best_score:
|
||||
best.append(x)
|
||||
return best
|
||||
|
||||
def argmin_random_tie(seq, fn):
|
||||
"""Return an element with lowest fn(seq[i]) score; break ties at random.
|
||||
Thus, for all s,f: argmin_random_tie(s, f) in argmin_list(s, f)"""
|
||||
best_score = fn(seq[0]); n = 0
|
||||
for x in seq:
|
||||
x_score = fn(x)
|
||||
if x_score < best_score:
|
||||
best, best_score = x, x_score; n = 1
|
||||
elif x_score == best_score:
|
||||
n += 1
|
||||
if random.randrange(n) == 0:
|
||||
best = x
|
||||
return best
|
||||
|
||||
def argmax(seq, fn):
|
||||
"""Return an element with highest fn(seq[i]) score; tie goes to first one.
|
||||
>>> argmax(['one', 'to', 'three'], len)
|
||||
'three'
|
||||
"""
|
||||
return argmin(seq, lambda x: -fn(x))
|
||||
|
||||
def argmax_list(seq, fn):
|
||||
"""Return a list of elements of seq[i] with the highest fn(seq[i]) scores.
|
||||
>>> argmax_list(['one', 'three', 'seven'], len)
|
||||
['three', 'seven']
|
||||
"""
|
||||
return argmin_list(seq, lambda x: -fn(x))
|
||||
|
||||
def argmax_random_tie(seq, fn):
|
||||
"Return an element with highest fn(seq[i]) score; break ties at random."
|
||||
return argmin_random_tie(seq, lambda x: -fn(x))
|
||||
#______________________________________________________________________________
|
||||
# Statistical and mathematical functions
|
||||
|
||||
def histogram(values, mode=0, bin_function=None):
|
||||
"""Return a list of (value, count) pairs, summarizing the input values.
|
||||
Sorted by increasing value, or if mode=1, by decreasing count.
|
||||
If bin_function is given, map it over values first."""
|
||||
if bin_function: values = map(bin_function, values)
|
||||
bins = {}
|
||||
for val in values:
|
||||
bins[val] = bins.get(val, 0) + 1
|
||||
if mode:
|
||||
return sorted(bins.items(), key=lambda v: v[1], reverse=True)
|
||||
else:
|
||||
return sorted(bins.items())
|
||||
|
||||
def log2(x):
|
||||
"""Base 2 logarithm.
|
||||
>>> log2(1024)
|
||||
10.0
|
||||
"""
|
||||
return math.log10(x) / math.log10(2)
|
||||
|
||||
def mode(values):
|
||||
"""Return the most common value in the list of values.
|
||||
>>> mode([1, 2, 3, 2])
|
||||
2
|
||||
"""
|
||||
return histogram(values, mode=1)[0][0]
|
||||
|
||||
def median(values):
|
||||
"""Return the middle value, when the values are sorted.
|
||||
If there are an odd number of elements, try to average the middle two.
|
||||
If they can't be averaged (e.g. they are strings), choose one at random.
|
||||
>>> median([10, 100, 11])
|
||||
11
|
||||
>>> median([1, 2, 3, 4])
|
||||
2.5
|
||||
"""
|
||||
n = len(values)
|
||||
values = sorted(values)
|
||||
if n % 2 == 1:
|
||||
return values[n/2]
|
||||
else:
|
||||
middle2 = values[(n/2)-1:(n/2)+1]
|
||||
try:
|
||||
return mean(middle2)
|
||||
except TypeError:
|
||||
return random.choice(middle2)
|
||||
|
||||
def mean(values):
|
||||
"""Return the arithmetic average of the values."""
|
||||
return sum(values) / float(len(values))
|
||||
|
||||
def stddev(values, meanval=None):
|
||||
"""The standard deviation of a set of values.
|
||||
Pass in the mean if you already know it."""
|
||||
if meanval == None: meanval = mean(values)
|
||||
return math.sqrt(sum([(x - meanval)**2 for x in values]) / (len(values)-1))
|
||||
|
||||
def dotproduct(X, Y):
|
||||
"""Return the sum of the element-wise product of vectors x and y.
|
||||
>>> dotproduct([1, 2, 3], [1000, 100, 10])
|
||||
1230
|
||||
"""
|
||||
return sum([x * y for x, y in zip(X, Y)])
|
||||
|
||||
def vector_add(a, b):
|
||||
"""Component-wise addition of two vectors.
|
||||
>>> vector_add((0, 1), (8, 9))
|
||||
(8, 10)
|
||||
"""
|
||||
return tuple(map(operator.add, a, b))
|
||||
|
||||
def probability(p):
|
||||
"Return true with probability p."
|
||||
return p > random.uniform(0.0, 1.0)
|
||||
|
||||
def num_or_str(x):
|
||||
"""The argument is a string; convert to a number if possible, or strip it.
|
||||
>>> num_or_str('42')
|
||||
42
|
||||
>>> num_or_str(' 42x ')
|
||||
'42x'
|
||||
"""
|
||||
if isnumber(x): return x
|
||||
try:
|
||||
return int(x)
|
||||
except ValueError:
|
||||
try:
|
||||
return float(x)
|
||||
except ValueError:
|
||||
return str(x).strip()
|
||||
|
||||
def normalize(numbers, total=1.0):
|
||||
"""Multiply each number by a constant such that the sum is 1.0 (or total).
|
||||
>>> normalize([1,2,1])
|
||||
[0.25, 0.5, 0.25]
|
||||
"""
|
||||
k = total / sum(numbers)
|
||||
return [k * n for n in numbers]
|
||||
|
||||
## OK, the following are not as widely useful utilities as some of the other
|
||||
## functions here, but they do show up wherever we have 2D grids: Wumpus and
|
||||
## Vacuum worlds, TicTacToe and Checkers, and markov decision Processes.
|
||||
|
||||
orientations = [(1,0), (0, 1), (-1, 0), (0, -1)]
|
||||
|
||||
def turn_right(orientation):
|
||||
return orientations[orientations.index(orientation)-1]
|
||||
|
||||
def turn_left(orientation):
|
||||
return orientations[(orientations.index(orientation)+1) % len(orientations)]
|
||||
|
||||
def distance((ax, ay), (bx, by)):
|
||||
"The distance between two (x, y) points."
|
||||
return math.hypot((ax - bx), (ay - by))
|
||||
|
||||
def distance2((ax, ay), (bx, by)):
|
||||
"The square of the distance between two (x, y) points."
|
||||
return (ax - bx)**2 + (ay - by)**2
|
||||
|
||||
def clip(vector, lowest, highest):
|
||||
"""Return vector, except if any element is less than the corresponding
|
||||
value of lowest or more than the corresponding value of highest, clip to
|
||||
those values.
|
||||
>>> clip((-1, 10), (0, 0), (9, 9))
|
||||
(0, 9)
|
||||
"""
|
||||
return type(vector)(map(min, map(max, vector, lowest), highest))
|
||||
#______________________________________________________________________________
|
||||
# Misc Functions
|
||||
|
||||
def printf(format, *args):
|
||||
"""Format args with the first argument as format string, and write.
|
||||
Return the last arg, or format itself if there are no args."""
|
||||
sys.stdout.write(str(format) % args)
|
||||
return if_(args, args[-1], format)
|
||||
|
||||
def caller(n=1):
|
||||
"""Return the name of the calling function n levels up in the frame stack.
|
||||
>>> caller(0)
|
||||
'caller'
|
||||
>>> def f():
|
||||
... return caller()
|
||||
>>> f()
|
||||
'f'
|
||||
"""
|
||||
import inspect
|
||||
return inspect.getouterframes(inspect.currentframe())[n][3]
|
||||
|
||||
def memoize(fn, slot=None):
|
||||
"""Memoize fn: make it remember the computed value for any argument list.
|
||||
If slot is specified, store result in that slot of first argument.
|
||||
If slot is false, store results in a dictionary."""
|
||||
if slot:
|
||||
def memoized_fn(obj, *args):
|
||||
if hasattr(obj, slot):
|
||||
return getattr(obj, slot)
|
||||
else:
|
||||
val = fn(obj, *args)
|
||||
setattr(obj, slot, val)
|
||||
return val
|
||||
else:
|
||||
def memoized_fn(*args):
|
||||
if not memoized_fn.cache.has_key(args):
|
||||
memoized_fn.cache[args] = fn(*args)
|
||||
return memoized_fn.cache[args]
|
||||
memoized_fn.cache = {}
|
||||
return memoized_fn
|
||||
|
||||
def if_(test, result, alternative):
|
||||
"""Like C++ and Java's (test ? result : alternative), except
|
||||
both result and alternative are always evaluated. However, if
|
||||
either evaluates to a function, it is applied to the empty arglist,
|
||||
so you can delay execution by putting it in a lambda.
|
||||
>>> if_(2 + 2 == 4, 'ok', lambda: expensive_computation())
|
||||
'ok'
|
||||
"""
|
||||
if test:
|
||||
if callable(result): return result()
|
||||
return result
|
||||
else:
|
||||
if callable(alternative): return alternative()
|
||||
return alternative
|
||||
|
||||
def name(object):
|
||||
"Try to find some reasonable name for the object."
|
||||
return (getattr(object, 'name', 0) or getattr(object, '__name__', 0)
|
||||
or getattr(getattr(object, '__class__', 0), '__name__', 0)
|
||||
or str(object))
|
||||
|
||||
def isnumber(x):
|
||||
"Is x a number? We say it is if it has a __int__ method."
|
||||
return hasattr(x, '__int__')
|
||||
|
||||
def issequence(x):
|
||||
"Is x a sequence? We say it is if it has a __getitem__ method."
|
||||
return hasattr(x, '__getitem__')
|
||||
|
||||
def print_table(table, header=None, sep=' ', numfmt='%g'):
|
||||
"""Print a list of lists as a table, so that columns line up nicely.
|
||||
header, if specified, will be printed as the first row.
|
||||
numfmt is the format for all numbers; you might want e.g. '%6.2f'.
|
||||
(If you want different formats in differnt columns, don't use print_table.)
|
||||
sep is the separator between columns."""
|
||||
justs = [if_(isnumber(x), 'rjust', 'ljust') for x in table[0]]
|
||||
if header:
|
||||
table = [header] + table
|
||||
table = [[if_(isnumber(x), lambda: numfmt % x, x) for x in row]
|
||||
for row in table]
|
||||
maxlen = lambda seq: max(map(len, seq))
|
||||
sizes = map(maxlen, zip(*[map(str, row) for row in table]))
|
||||
for row in table:
|
||||
for (j, size, x) in zip(justs, sizes, row):
|
||||
print getattr(str(x), j)(size), sep,
|
||||
print
|
||||
|
||||
def AIMAFile(components, mode='r'):
|
||||
"Open a file based at the AIMA root directory."
|
||||
import utils
|
||||
dir = os.path.dirname(utils.__file__)
|
||||
return open(apply(os.path.join, [dir] + components), mode)
|
||||
|
||||
def DataFile(name, mode='r'):
|
||||
"Return a file in the AIMA /data directory."
|
||||
return AIMAFile(['..', 'data', name], mode)
|
||||
|
||||
|
||||
#______________________________________________________________________________
|
||||
# Queues: Stack, FIFOQueue, PriorityQueue
|
||||
|
||||
class Queue:
|
||||
"""Queue is an abstract class/interface. There are three types:
|
||||
Stack(): A Last In First Out Queue.
|
||||
FIFOQueue(): A First In First Out Queue.
|
||||
PriorityQueue(lt): Queue where items are sorted by lt, (default <).
|
||||
Each type supports the following methods and functions:
|
||||
q.append(item) -- add an item to the queue
|
||||
q.extend(items) -- equivalent to: for item in items: q.append(item)
|
||||
q.pop() -- return the top item from the queue
|
||||
len(q) -- number of items in q (also q.__len())
|
||||
Note that isinstance(Stack(), Queue) is false, because we implement stacks
|
||||
as lists. If Python ever gets interfaces, Queue will be an interface."""
|
||||
|
||||
def __init__(self):
|
||||
abstract
|
||||
|
||||
def extend(self, items):
|
||||
for item in items: self.append(item)
|
||||
|
||||
def Stack():
|
||||
"""Return an empty list, suitable as a Last-In-First-Out Queue."""
|
||||
return []
|
||||
|
||||
class FIFOQueue(Queue):
|
||||
"""A First-In-First-Out Queue."""
|
||||
def __init__(self):
|
||||
self.A = []; self.start = 0
|
||||
def append(self, item):
|
||||
self.A.append(item)
|
||||
def __len__(self):
|
||||
return len(self.A) - self.start
|
||||
def extend(self, items):
|
||||
self.A.extend(items)
|
||||
def pop(self):
|
||||
e = self.A[self.start]
|
||||
self.start += 1
|
||||
if self.start > 5 and self.start > len(self.A)/2:
|
||||
self.A = self.A[self.start:]
|
||||
self.start = 0
|
||||
return e
|
||||
|
||||
class PriorityQueue(Queue):
|
||||
"""A queue in which the minimum (or maximum) element (as determined by f and
|
||||
order) is returned first. If order is min, the item with minimum f(x) is
|
||||
returned first; if order is max, then it is the item with maximum f(x)."""
|
||||
def __init__(self, order=min, f=lambda x: x):
|
||||
update(self, A=[], order=order, f=f)
|
||||
def append(self, item):
|
||||
bisect.insort(self.A, (self.f(item), item))
|
||||
def __len__(self):
|
||||
return len(self.A)
|
||||
def pop(self):
|
||||
if self.order == min:
|
||||
return self.A.pop(0)[1]
|
||||
else:
|
||||
return self.A.pop()[1]
|
||||
|
||||
## Fig: The idea is we can define things like Fig[3,10] later.
|
||||
## Alas, it is Fig[3,10] not Fig[3.10], because that would be the same as Fig[3.1]
|
||||
Fig = {}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue