0% found this document useful (0 votes)
4 views

Stochastic Local Search Python

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views

Stochastic Local Search Python

Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 23

Chapter 4

Reasoning with Constraints

4.1 Constraint Satisfaction Problems


4.1.1 Variables
A variable consists of a name, a domain and an optional (x,y) position (for
displaying). The domain of a variable is a list or a tuple, as the ordering will
matter in the representation of constraints.
variable.py — Representations of a variable in CSPs and probabilistic models
11 import random
12
13 class Variable(object):
14 """A random variable.
15 name (string) - name of the variable
16 domain (list) - a list of the values for the variable.
17 Variables are ordered according to their name.
18 """
19
20 def __init__(self, name, domain, position=None):
21 """Variable
22 name a string
23 domain a list of printable values
24 position of form (x,y)
25 """
26 self.name = name # string
27 self.domain = domain # list of values
28 self.position = position if position else (random.random(),
random.random())
29 self.size = len(domain)
30
31 def __str__(self):

69
70 4. Reasoning with Constraints

32 return self.name
33
34 def __repr__(self):
35 return self.name # f"Variable({self.name})"

4.1.2 Constraints
A constraint consists of:

• A tuple (or list) of variables called the scope.

• A condition, a Boolean function that takes the same number of argu-


ments as there are variables in the scope. The condition must have a
__name__ property that gives a printable name of the function; built-in
functions and functions that are defined using def have such a property;
for other functions you may need to define this property.

• An optional name

• An optional (x, y) position

cspProblem.py — Representations of a Constraint Satisfaction Problem


11 from variable import Variable
12
13 # for showing csps:
14 import matplotlib.pyplot as plt
15 import matplotlib.lines as lines
16
17 class Constraint(object):
18 """A Constraint consists of
19 * scope: a tuple of variables
20 * condition: a Boolean function that can applied to a tuple of values
for variables in scope
21 * string: a string for printing the constraints. All of the strings
must be unique.
22 for the variables
23 """
24 def __init__(self, scope, condition, string=None, position=None):
25 self.scope = scope
26 self.condition = condition
27 if string is None:
28 self.string = f"{self.condition.__name__}({self.scope})"
29 else:
30 self.string = string
31 self.position = position
32
33 def __repr__(self):
34 return self.string

https://aipython.org Version 0.9.13 May 7, 2024


4.1. Constraint Satisfaction Problems 71

An assignment is a variable:value dictionary.


If con is a constraint, con.holds(assignment) returns True or False depending
on whether the condition is true or false for that assignment. The assignment
assignment must assign a value to every variable in the scope of the constraint
con (and could also assign values to other variables); con.holds gives an error if
not all variables in the scope of con are assigned in the assignment. It ignores
variables in assignment that are not in the scope of the constraint.
In Python, the ∗ notation is used for unpacking a tuple. For example,
F(∗(1, 2, 3)) is the same as F(1, 2, 3). So if t has value (1, 2, 3), then F(∗t) is
the same as F(1, 2, 3).
cspProblem.py — (continued)

36 def can_evaluate(self, assignment):


37 """
38 assignment is a variable:value dictionary
39 returns True if the constraint can be evaluated given assignment
40 """
41 return all(v in assignment for v in self.scope)
42
43 def holds(self,assignment):
44 """returns the value of Constraint con evaluated in assignment.
45
46 precondition: all variables are assigned in assignment, ie
self.can_evaluate(assignment) is true
47 """
48 return self.condition(*tuple(assignment[v] for v in self.scope))

4.1.3 CSPs
A constraint satisfaction problem (CSP) requires:

• variables: a list or set of variables

• constraints: a set or list of constraints.

Other properties are inferred from these:

• var to const is a mapping from variables to set of constraints, such that


var to const[var] is the set of constraints with var in the scope.

cspProblem.py — (continued)

50 class CSP(object):
51 """A CSP consists of
52 * a title (a string)
53 * variables, a set of variables
54 * constraints, a list of constraints
55 * var_to_const, a variable to set of constraints dictionary
56 """

https://aipython.org Version 0.9.13 May 7, 2024


72 4. Reasoning with Constraints

57 def __init__(self, title, variables, constraints):


58 """title is a string
59 variables is set of variables
60 constraints is a list of constraints
61 """
62 self.title = title
63 self.variables = variables
64 self.constraints = constraints
65 self.var_to_const = {var:set() for var in self.variables}
66 for con in constraints:
67 for var in con.scope:
68 self.var_to_const[var].add(con)
69
70 def __str__(self):
71 """string representation of CSP"""
72 return str(self.title)
73
74 def __repr__(self):
75 """more detailed string representation of CSP"""
76 return f"CSP({self.title}, {self.variables}, {([str(c) for c in
self.constraints])})"
csp.consistent(assignment) returns true if the assignment is consistent with each
of the constraints in csp (i.e., all of the constraints that can be evaluated evaluate
to true). Note that this is a local consistency with each constraint; it does not
imply the CSP is consistent or has a solution.
cspProblem.py — (continued)

78 def consistent(self,assignment):
79 """assignment is a variable:value dictionary
80 returns True if all of the constraints that can be evaluated
81 evaluate to True given assignment.
82 """
83 return all(con.holds(assignment)
84 for con in self.constraints
85 if con.can_evaluate(assignment))
The show method uses matplotlib to show the graphical structure of a con-
straint network. If the node positions are not specified, this gives different
positions each time it is run; if you don’t like the graph, try again.
cspProblem.py — (continued)

87 def show(self, linewidth=3, showDomains=False, showAutoAC = False):


88 self.linewidth = linewidth
89 self.picked = None
90 plt.ion() # interactive
91 self.arcs = {} # arc: (con,var) dictionary
92 self.thelines = {} # (con,var):arc dictionary
93 self.nodes = {} # node: variable dictionary
94 self.fig, self.ax= plt.subplots(1, 1)
95 self.ax.set_axis_off()

https://aipython.org Version 0.9.13 May 7, 2024


4.1. Constraint Satisfaction Problems 73

96 for var in self.variables:


97 if var.position is None:
98 var.position = (random.random(), random.random())
99 self.showAutoAC = showAutoAC # used for consistency GUI
100 self.autoAC = False
101 domains = {var:var.domain for var in self.variables} if showDomains
else {}
102 self.draw_graph(domains=domains)
103
104 def draw_graph(self, domains={}, to_do = {}, title=None, fontsize=10):
105 self.ax.clear()
106 self.ax.set_axis_off()
107 if title:
108 plt.title(title, fontsize=fontsize)
109 else:
110 plt.title(self.title, fontsize=fontsize)
111 var_bbox = dict(boxstyle="round4,pad=1.0,rounding_size=0.5")
112 con_bbox = dict(boxstyle="square,pad=1.0",color="green")
113 self.autoACtext = plt.text(0,0,"Auto AC" if self.showAutoAC else "",
114 bbox={'boxstyle':'square','color':'yellow'},
115 picker=True, fontsize=fontsize)
116 for con in self.constraints:
117 if con.position is None:
118 con.position = tuple(sum(var.position[i] for var in
con.scope)/len(con.scope)
119 for i in range(2))
120 cx,cy = con.position
121 bbox = dict(boxstyle="square,pad=1.0",color="green")
122 for var in con.scope:
123 vx,vy = var.position
124 if (var,con) in to_do:
125 color = 'blue'
126 else:
127 color = 'limegreen'
128 line = lines.Line2D([cx,vx], [cy,vy], axes=self.ax,
color=color,
129 picker=True, pickradius=10,
linewidth=self.linewidth)
130 self.arcs[line]= (var,con)
131 self.thelines[(var,con)] = line
132 self.ax.add_line(line)
133 plt.text(cx,cy,con.string,
134 bbox=con_bbox,
135 ha='center',va='center', fontsize=fontsize)
136 for var in self.variables:
137 x,y = var.position
138 if domains:
139 node_label = f"{var.name}\n{domains[var]}"
140 else:
141 node_label = var.name

https://aipython.org Version 0.9.13 May 7, 2024


74 4. Reasoning with Constraints

142 node = plt.text(x, y, node_label, bbox=var_bbox, ha='center',


va='center',
143 picker=True, fontsize=fontsize)
144 self.nodes[node] = var
145 self.fig.canvas.mpl_connect('pick_event', self.pick_handler)
146
147 def pick_handler(self,event):
148 mouseevent = event.mouseevent
149 self.last_artist = artist = event.artist
150 #print('***picker handler:',artist, 'mouseevent:', mouseevent)
151 if artist in self.arcs:
152 #print('### selected arc',self.arcs[artist])
153 self.picked = self.arcs[artist]
154 elif artist in self.nodes:
155 #print('### selected node',self.nodes[artist])
156 self.picked = self.nodes[artist]
157 elif artist==self.autoACtext:
158 self.autoAC = True
159 #print("*** autoAC")
160 else:
161 print("### unknown click")

4.1.4 Examples
In the following code ne , when given a number, returns a function that is true
when its argument is not that number. For example, if f = ne (3), then f (2)
is True and f (3) is False. That is, ne (x)(y) is true when x ̸= y. Allowing
a function of multiple arguments to use its arguments one at a time is called
currying, after the logician Haskell Curry. Functions used as conditions in
constraints require names (so they can be printed).
cspExamples.py — Example CSPs
11 from cspProblem import Variable, CSP, Constraint
12 from operator import lt,ne,eq,gt
13
14 def ne_(val):
15 """not equal value"""
16 # nev = lambda x: x != val # alternative definition
17 # nev = partial(neq,val) # another alternative definition
18 def nev(x):
19 return val != x
20 nev.__name__ = f"{val} != " # name of the function
21 return nev
Similarly is (x)(y) is true when x = y.
cspExamples.py — (continued)

23 def is_(val):
24 """is a value"""
25 # isv = lambda x: x == val # alternative definition

https://aipython.org Version 0.9.13 May 7, 2024


4.1. Constraint Satisfaction Problems 75

csp1
A B B != 2

C
A<B

B<C

Figure 4.1: csp1.show()

26 # isv = partial(eq,val) # another alternative definition


27 def isv(x):
28 return val == x
29 isv.__name__ = f"{val} == "
30 return isv
The CSP, csp0 has variables X, Y and Z, each with domain {1, 2, 3}. The con-
straints are X < Y and Y < Z.
cspExamples.py — (continued)

32 X = Variable('X', {1,2,3})
33 Y = Variable('Y', {1,2,3})
34 Z = Variable('Z', {1,2,3})
35 csp0 = CSP("csp0", {X,Y,Z},
36 [ Constraint([X,Y],lt),
37 Constraint([Y,Z],lt)])
The CSP, csp1 has variables A, B and C, each with domain {1, 2, 3, 4}. The con-
straints are A < B, B ̸= 2, and B < C. This is slightly more interesting than
csp0 as it has more solutions. This example is used in the unit tests, and so if it
is changed, the unit tests need to be changed. The CSP csp1s is the same, but
with only the constraints A < B and B < C
cspExamples.py — (continued)

39 A = Variable('A', {1,2,3,4}, position=(0.2,0.9))


40 B = Variable('B', {1,2,3,4}, position=(0.8,0.9))
41 C = Variable('C', {1,2,3,4}, position=(1,0.4))
42 C0 = Constraint([A,B], lt, "A < B", position=(0.4,0.3))
43 C1 = Constraint([B], ne_(2), "B != 2", position=(1,0.9))
44 C2 = Constraint([B,C], lt, "B < C", position=(0.6,0.1))

https://aipython.org Version 0.9.13 May 7, 2024


76 4. Reasoning with Constraints

csp2

A A != B B B != 3

A=D B != D A != C

A>E B>E
D C<D C

D>E C>E C != 2

Figure 4.2: csp2.show()

45 csp1 = CSP("csp1", {A, B, C},


46 [C0, C1, C2])
47
48 csp1s = CSP("csp1s", {A, B, C},
49 [C0, C2]) # A<B, B<C

The next CSP, csp2 is Example 4.9 of Poole and Mackworth [2023]; the do-
main consistent network (after applying the unary constraints) is shown in Fig-
ure 4.2. Note that we use the same variables as the previous example and add
two more.

cspExamples.py — (continued)

51 D = Variable('D', {1,2,3,4}, position=(0,0.4))


52 E = Variable('E', {1,2,3,4}, position=(0.5,0))
53 csp2 = CSP("csp2", {A,B,C,D,E},
54 [ Constraint([B], ne_(3), "B != 3", position=(1,0.9)),
55 Constraint([C], ne_(2), "C != 2", position=(1,0.2)),
56 Constraint([A,B], ne, "A != B"),
57 Constraint([B,C], ne, "A != C"),
58 Constraint([C,D], lt, "C < D"),
59 Constraint([A,D], eq, "A = D"),
60 Constraint([E,A], lt, "E < A"),
61 Constraint([E,B], lt, "E < B"),
62 Constraint([E,C], lt, "E < C"),
63 Constraint([E,D], lt, "E < D"),

https://aipython.org Version 0.9.13 May 7, 2024


4.1. Constraint Satisfaction Problems 77

csp3

A A != B B

A<D

A-E is odd B<E


D D<C C

D != E C != E

Figure 4.3: csp3.show()

64 Constraint([B,D], ne, "B != D")])

The following example is another scheduling problem (but with multiple an-
swers). This is the same as “scheduling 2” in the original AIspace.org consis-
tency app.

cspExamples.py — (continued)

66 csp3 = CSP("csp3", {A,B,C,D,E},


67 [Constraint([A,B], ne, "A != B"),
68 Constraint([A,D], lt, "A < D"),
69 Constraint([A,E], lambda a,e: (a-e)%2 == 1, "A-E is odd"),
70 Constraint([B,E], lt, "B < E"),
71 Constraint([D,C], lt, "D < C"),
72 Constraint([C,E], ne, "C != E"),
73 Constraint([D,E], ne, "D != E")])

The following example is another abstract scheduling problem. What are


the solutions?

cspExamples.py — (continued)

75 def adjacent(x,y):
76 """True when x and y are adjacent numbers"""
77 return abs(x-y) == 1

https://aipython.org Version 0.9.13 May 7, 2024


78 4. Reasoning with Constraints

csp4
A adjacent(A,B) B

B != D A != C adjacent(B,C)

D adjacent(C,D) C

adjacent(D,E) C != E

Figure 4.4: csp4.show()

78
79 csp4 = CSP("csp4", {A,B,C,D},
80 [Constraint([A,B], adjacent, "adjacent(A,B)"),
81 Constraint([B,C], adjacent, "adjacent(B,C)"),
82 Constraint([C,D], adjacent, "adjacent(C,D)"),
83 Constraint([A,C], ne, "A != C"),
84 Constraint([B,D], ne, "B != D") ])
The following examples represent the crossword shown in Figure 4.5.
In the first representation, the variables represent words. The constraint
imposed by the crossword is that where two words intersect, the letter at the
intersection must be the same. The method meet_at is used to test whether two
words intersect with the same letter. For example, the constraint meet_at(2,0)
means that the third letter (at position 2) of the first argument is the same as
the first letter of the second argument. This is shown in Figure 4.6.
cspExamples.py — (continued)

86 def meet_at(p1,p2):
87 """returns a function of two words that is true
88 when the words intersect at positions p1, p2.
89 The positions are relative to the words; starting at position 0.
90 meet_at(p1,p2)(w1,w2) is true if the same letter is at position p1 of
word w1
91 and at position p2 of word w2.
92 """
93 def meets(w1,w2):
94 return w1[p1] == w2[p2]

https://aipython.org Version 0.9.13 May 7, 2024


4.1. Constraint Satisfaction Problems 79

1 2

Words:
3
ant, big, bus, car, has,
book, buys, hold, lane,
year, ginger, search,
symbol, syntax.
4

Figure 4.5: crossword1: a crossword puzzle to be solved

crossword1

one_across
meet_at(2,0)[one_across, two_down]
meet_at(0,0)[one_across, one_down] two_down

one_down

meet_at(2,2)[three_across, two_down]
meet_at(0,2)[three_across, one_down]

meet_at(0,4)[four_across, two_down]

three_across

four_across

Figure 4.6: crossword1.show()

https://aipython.org Version 0.9.13 May 7, 2024


80 4. Reasoning with Constraints

95 meets.__name__ = f"meet_at({p1},{p2})"
96 return meets
97
98 one_across = Variable('one_across', {'ant', 'big', 'bus', 'car', 'has'},
position=(0.3,0.9))
99 one_down = Variable('one_down', {'book', 'buys', 'hold', 'lane', 'year'},
position=(0.1,0.7))
100 two_down = Variable('two_down', {'ginger', 'search', 'symbol', 'syntax'},
position=(0.9,0.8))
101 three_across = Variable('three_across', {'book', 'buys', 'hold', 'land',
'year'}, position=(0.1,0.3))
102 four_across = Variable('four_across',{'ant', 'big', 'bus', 'car', 'has'},
position=(0.7,0.0))
103 crossword1 = CSP("crossword1",
104 {one_across, one_down, two_down, three_across,
four_across},
105 [Constraint([one_across,one_down], meet_at(0,0)),
106 Constraint([one_across,two_down], meet_at(2,0)),
107 Constraint([three_across,two_down], meet_at(2,2)),
108 Constraint([three_across,one_down], meet_at(0,2)),
109 Constraint([four_across,two_down], meet_at(0,4))])
In an alternative representation of a crossword (the “dual” representation),
the variables represent letters, and the constraints are that adjacent sequences
of letters form words. This is shown in Figure 4.7.
cspExamples.py — (continued)

111 words = {'ant', 'big', 'bus', 'car', 'has','book', 'buys', 'hold',


112 'lane', 'year', 'ginger', 'search', 'symbol', 'syntax'}
113
114 def is_word(*letters, words=words):
115 """is true if the letters concatenated form a word in words"""
116 return "".join(letters) in words
117
118 letters = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l",
119 "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y",
120 "z"}
121
122 # pij is the variable representing the letter i from the left and j down
(starting from 0)
123 p00 = Variable('p00', letters, position=(0.1,0.85))
124 p10 = Variable('p10', letters, position=(0.3,0.85))
125 p20 = Variable('p20', letters, position=(0.5,0.85))
126 p01 = Variable('p01', letters, position=(0.1,0.7))
127 p21 = Variable('p21', letters, position=(0.5,0.7))
128 p02 = Variable('p02', letters, position=(0.1,0.55))
129 p12 = Variable('p12', letters, position=(0.3,0.55))
130 p22 = Variable('p22', letters, position=(0.5,0.55))
131 p32 = Variable('p32', letters, position=(0.7,0.55))
132 p03 = Variable('p03', letters, position=(0.1,0.4))
133 p23 = Variable('p23', letters, position=(0.5,0.4))

https://aipython.org Version 0.9.13 May 7, 2024


4.1. Constraint Satisfaction Problems 81

crossword1d
is_word[p00, p10, p20]

p00 p10 p20

p01 p21

is_word[p00, p01, p02, p03] is_word[p02, p12, p22, p32]

p02 p12 p22 p32

is_word[p20, p21, p22, p23, p24, p25]

p03 p23

is_word[p24, p34, p44]

p24 p34 p44

p25

Figure 4.7: crossword1d.show()

134 p24 = Variable('p24', letters, position=(0.5,0.25))


135 p34 = Variable('p34', letters, position=(0.7,0.25))
136 p44 = Variable('p44', letters, position=(0.9,0.25))
137 p25 = Variable('p25', letters, position=(0.5,0.1))
138
139 crossword1d = CSP("crossword1d",
140 {p00, p10, p20, # first row
141 p01, p21, # second row
142 p02, p12, p22, p32, # third row
143 p03, p23, #fourth row
144 p24, p34, p44, # fifth row
145 p25 # sixth row
146 },
147 [Constraint([p00, p10, p20], is_word,
position=(0.3,0.95)), #1-across
148 Constraint([p00, p01, p02, p03], is_word,
position=(0,0.625)), # 1-down
149 Constraint([p02, p12, p22, p32], is_word,
position=(0.3,0.625)), # 3-across
150 Constraint([p20, p21, p22, p23, p24, p25], is_word,
position=(0.45,0.475)), # 2-down
151 Constraint([p24, p34, p44], is_word,
position=(0.7,0.325)) # 4-across

https://aipython.org Version 0.9.13 May 7, 2024


82 4. Reasoning with Constraints

152 ])

Exercise 4.1 How many assignments of a value to each variable are there for
each of the representations of the above crossword? Do you think an exhaustive
enumeration will work for either one?
The queens problem is a puzzle on a chess board, where the idea is to place
a queen on each column so the queens cannot take each other: there are no
two queens on the same row, column or diagonal. The n-queens problem is a
generalization where the size of the board is an n × n, and n queens have to be
placed.
Here is a representation of the n-queens problem, where the variables are
the columns and the values are the rows in which the queen is placed. The
original queens problem on a standard (8 × 8) chess board is n_queens(8)
cspExamples.py — (continued)

154 def queens(ri,rj):


155 """ri and rj are different rows, return the condition that the queens
cannot take each other"""
156 def no_take(ci,cj):
157 """is true if queen at (ri,ci) cannot take a queen at (rj,cj)"""
158 return ci != cj and abs(ri-ci) != abs(rj-cj)
159 return no_take
160
161 def n_queens(n):
162 """returns a CSP for n-queens"""
163 columns = list(range(n))
164 variables = [Variable(f"R{i}",columns) for i in range(n)]
165 return CSP("n-queens",
166 variables,
167 [Constraint([variables[i], variables[j]], queens(i,j))
168 for i in range(n) for j in range(n) if i != j])
169
170 # try the CSP n_queens(8) in one of the solvers.
171 # What is the smallest n for which there is a solution?

Exercise 4.2 How many constraints does this representation of the n-queens
problem produce? Can it be done with fewer constraints? Either explain why it
can’t be done with fewer constraints, or give a solution using fewer constraints.

Unit tests
The following defines a unit test for csp solvers, by default using example csp1.
cspExamples.py — (continued)

173 def test_csp(CSP_solver, csp=csp1,


174 solutions=[{A: 1, B: 3, C: 4}, {A: 2, B: 3, C: 4}]):
175 """CSP_solver is a solver that takes a csp and returns a solution
176 csp is a constraint satisfaction problem
177 solutions is the list of all solutions to csp

https://aipython.org Version 0.9.13 May 7, 2024


4.2. A Simple Depth-first Solver 83

178 This tests whether the solution returned by CSP_solver is a solution.


179 """
180 print("Testing csp with",CSP_solver.__doc__)
181 sol0 = CSP_solver(csp)
182 print("Solution found:",sol0)
183 assert sol0 in solutions, f"Solution not correct for {csp}"
184 print("Passed unit test")

Exercise 4.3 Modify test so that instead of taking in a list of solutions, it checks
whether the returned solution actually is a solution.
Exercise 4.4 Propose a test that is appropriate for CSPs with no solutions. As-
sume that the test designer knows there are no solutions. Consider what a CSP
solver should return if there are no solutions to the CSP.
Exercise 4.5 Write a unit test that checks whether all solutions (e.g., for the search
algorithms that can return multiple solutions) are correct, and whether all solu-
tions can be found.

4.2 A Simple Depth-first Solver


The first solver carries out a depth-first search through the space of partial as-
signments. This takes in a CSP problem and an optional variable ordering (a
list of the variables in the CSP). It returns a generator of the solutions (see Sec-
tion 1.5.4 on yield for enumerations).
cspDFS.py — Solving a CSP using depth-first search.
11 import cspExamples
12
13 def dfs_solver(constraints, context, var_order):
14 """generator for all solutions to csp.
15 context is an assignment of values to some of the variables.
16 var_order is a list of the variables in csp that are not in context.
17 """
18 to_eval = {c for c in constraints if c.can_evaluate(context)}
19 if all(c.holds(context) for c in to_eval):
20 if var_order == []:
21 yield context
22 else:
23 rem_cons = [c for c in constraints if c not in to_eval]
24 var = var_order[0]
25 for val in var.domain:
26 yield from dfs_solver(rem_cons, context|{var:val},
var_order[1:])
27
28 def dfs_solve_all(csp, var_order=None):
29 """depth-first CSP solver to return a list of all solutions to csp.
30 """
31 if var_order == None: # use an arbitrary variable order
32 var_order = list(csp.variables)

https://aipython.org Version 0.9.13 May 7, 2024


4.5. Solving CSPs using Stochastic Local Search 95

188 sol = Searcher(Search_with_AC_from_CSP(csp)).search()


189 if sol:
190 return {v:select(d) for (v,d) in sol.end().items()}
191
192 if __name__ == "__main__":
193 cspExamples.test_csp(ac_search_solver)
Testing:
cspConsistency.py — (continued)

195 ## Test Solving CSPs with Arc consistency and domain splitting:
196 #Con_solver.max_display_level = 4 # display details of AC (0 turns off)
197 #Con_solver(cspExamples.csp1).solve_all()
198 #searcher1d = Searcher(Search_with_AC_from_CSP(cspExamples.csp1))
199 #print(searcher1d.search())
200 #Searcher.max_display_level = 2 # display search trace (0 turns off)
201 #searcher2c = Searcher(Search_with_AC_from_CSP(cspExamples.csp2))
202 #print(searcher2c.search())
203 #searcher3c = Searcher(Search_with_AC_from_CSP(cspExamples.crossword1))
204 #print(searcher3c.search())
205 #searcher4c = Searcher(Search_with_AC_from_CSP(cspExamples.crossword1d))
206 #print(searcher4c.search())

4.5 Solving CSPs using Stochastic Local Search


To run the demo, in folder ”aipython”, load ”cspSLS.py”, and copy
and paste the commented-out example queries at the bottom of that
file. This assumes Python 3. Some of the queries require matplotlib.

The following code implements the two-stage choice (select one of the vari-
ables that are involved in the most constraints that are violated, then a value),
the any-conflict algorithm (select a variable that participates in a violated con-
straint) and a random choice of variable, as well as a probabilistic mix of the
three.
Given a CSP, the stochastic local searcher (SLSearcher) creates the data struc-
tures:

• variables to select is the set of all of the variables with domain-size greater
than one. For a variable not in this set, we cannot pick another value from
that variable.

• var to constraints maps from a variable into the set of constraints it is in-
volved in. Note that the inverse mapping from constraints into variables
is part of the definition of a constraint.

cspSLS.py — Stochastic Local Search for Solving CSPs


11 from cspProblem import CSP, Constraint

https://aipython.org Version 0.9.13 May 7, 2024


96 4. Reasoning with Constraints

12 from searchProblem import Arc, Search_problem


13 from display import Displayable
14 import random
15 import heapq
16
17 class SLSearcher(Displayable):
18 """A search problem directly from the CSP..
19
20 A node is a variable:value dictionary"""
21 def __init__(self, csp):
22 self.csp = csp
23 self.variables_to_select = {var for var in self.csp.variables
24 if len(var.domain) > 1}
25 # Create assignment and conflicts set
26 self.current_assignment = None # this will trigger a random restart
27 self.number_of_steps = 0 #number of steps after the initialization

restart creates a new total assignment, and constructs the set of conflicts (the
constraints that are false in this assignment).

cspSLS.py — (continued)

29 def restart(self):
30 """creates a new total assignment and the conflict set
31 """
32 self.current_assignment = {var:random_choice(var.domain) for
33 var in self.csp.variables}
34 self.display(2,"Initial assignment",self.current_assignment)
35 self.conflicts = set()
36 for con in self.csp.constraints:
37 if not con.holds(self.current_assignment):
38 self.conflicts.add(con)
39 self.display(2,"Number of conflicts",len(self.conflicts))
40 self.variable_pq = None

The search method is the top-level searching algorithm. It can either be used
to start the search or to continue searching. If there is no current assignment,
it must create one. Note that, when counting steps, a restart is counted as one
step, which is not appropriate for CSPs with many variables, as it is a relatively
expensive operation for these cases.
This method selects one of two implementations. The argument pob best
is the probability of selecting a best variable (one involving the most conflicts).
When the value of prob best is positive, the algorithm needs to maintain a prior-
ity queue of variables and the number of conflicts (using search with var pq). If
the probability of selecting a best variable is zero, it does not need to maintain
this priority queue (as implemented in search with any conflict).
The argument prob anycon is the probability that the any-conflict strategy is
used (which selects a variable at random that is in a conflict), assuming that
it is not picking a best variable. Note that for the probability parameters, any
value less that zero acts like probability zero and any value greater than 1 acts

https://aipython.org Version 0.9.13 May 7, 2024


4.5. Solving CSPs using Stochastic Local Search 97

like probability 1. This means that when prob anycon = 1.0, a best variable is
chosen with probability prob best, otherwise a variable in any conflict is chosen.
A variable is chosen at random with probability 1 − prob anycon − prob best as
long as that is positive.
This returns the number of steps needed to find a solution, or None if no
solution is found. If there is a solution, it is in self .current assignment.
cspSLS.py — (continued)

42 def search(self,max_steps, prob_best=0, prob_anycon=1.0):


43 """
44 returns the number of steps or None if these is no solution.
45 If there is a solution, it can be found in self.current_assignment
46
47 max_steps is the maximum number of steps it will try before giving
up
48 prob_best is the probability that a best variable (one in most
conflict) is selected
49 prob_anycon is the probability that a variable in any conflict is
selected
50 (otherwise a variable is chosen at random)
51 """
52 if self.current_assignment is None:
53 self.restart()
54 self.number_of_steps += 1
55 if not self.conflicts:
56 self.display(1,"Solution found:", self.current_assignment,
"after restart")
57 return self.number_of_steps
58 if prob_best > 0: # we need to maintain a variable priority queue
59 return self.search_with_var_pq(max_steps, prob_best,
prob_anycon)
60 else:
61 return self.search_with_any_conflict(max_steps, prob_anycon)

Exercise 4.13 This does an initial random assignment but does not do any ran-
dom restarts. Implement a searcher that takes in the maximum number of walk
steps (corresponding to existing max steps) and the maximum number of restarts,
and returns the total number of steps for the first solution found. (As in search, the
solution found can be extracted from the variable self .current assignment).

4.5.1 Any-conflict
In the any-conflict heuristic a variable that participates in a violated constraint
is picked at random. The implementation need to keeps track of which vari-
ables are in conflicts. This is can avoid the need for a priority queue that is
needed when the probability of picking a best variable is greter than zero.
cspSLS.py — (continued)

63 def search_with_any_conflict(self, max_steps, prob_anycon=1.0):

https://aipython.org Version 0.9.13 May 7, 2024


98 4. Reasoning with Constraints

64 """Searches with the any_conflict heuristic.


65 This relies on just maintaining the set of conflicts;
66 it does not maintain a priority queue
67 """
68 self.variable_pq = None # we are not maintaining the priority queue.
69 # This ensures it is regenerated if
70 # we call search_with_var_pq.
71 for i in range(max_steps):
72 self.number_of_steps +=1
73 if random.random() < prob_anycon:
74 con = random_choice(self.conflicts) # pick random conflict
75 var = random_choice(con.scope) # pick variable in conflict
76 else:
77 var = random_choice(self.variables_to_select)
78 if len(var.domain) > 1:
79 val = random_choice([val for val in var.domain
80 if val is not
self.current_assignment[var]])
81 self.display(2,self.number_of_steps,":
Assigning",var,"=",val)
82 self.current_assignment[var]=val
83 for varcon in self.csp.var_to_const[var]:
84 if varcon.holds(self.current_assignment):
85 if varcon in self.conflicts:
86 self.conflicts.remove(varcon)
87 else:
88 if varcon not in self.conflicts:
89 self.conflicts.add(varcon)
90 self.display(2," Number of conflicts",len(self.conflicts))
91 if not self.conflicts:
92 self.display(1,"Solution found:", self.current_assignment,
93 "in", self.number_of_steps,"steps")
94 return self.number_of_steps
95 self.display(1,"No solution in",self.number_of_steps,"steps",
96 len(self.conflicts),"conflicts remain")
97 return None

Exercise 4.14 This makes no attempt to find the best value for the variable se-
lected. Modify the code to include an option selects a value for the selected vari-
able that reduces the number of conflicts the most. Have a parameter that specifies
the probability that the best value is chosen, and otherwise chooses a value at ran-
dom.

4.5.2 Two-Stage Choice


This is the top-level searching algorithm that maintains a priority queue of
variables ordered by the number of conflicts, so that the variable with the most
conflicts is selected first. If there is no current priority queue of variables, one
is created.

https://aipython.org Version 0.9.13 May 7, 2024


4.5. Solving CSPs using Stochastic Local Search 99

The main complexity here is to maintain the priority queue. When a vari-
able var is assigned a value val, for each constraint that has become satisfied
or unsatisfied, each variable involved in the constraint need to have its count
updated. The change is recorded in the dictionary var differential, which is used
to update the priority queue (see Section 4.5.3).

cspSLS.py — (continued)

99 def search_with_var_pq(self,max_steps, prob_best=1.0, prob_anycon=1.0):


100 """search with a priority queue of variables.
101 This is used to select a variable with the most conflicts.
102 """
103 if not self.variable_pq:
104 self.create_pq()
105 pick_best_or_con = prob_best + prob_anycon
106 for i in range(max_steps):
107 self.number_of_steps +=1
108 randnum = random.random()
109 ## Pick a variable
110 if randnum < prob_best: # pick best variable
111 var,oldval = self.variable_pq.top()
112 elif randnum < pick_best_or_con: # pick a variable in a conflict
113 con = random_choice(self.conflicts)
114 var = random_choice(con.scope)
115 else: #pick any variable that can be selected
116 var = random_choice(self.variables_to_select)
117 if len(var.domain) > 1: # var has other values
118 ## Pick a value
119 val = random_choice([val for val in var.domain if val is not
120 self.current_assignment[var]])
121 self.display(2,"Assigning",var,val)
122 ## Update the priority queue
123 var_differential = {}
124 self.current_assignment[var]=val
125 for varcon in self.csp.var_to_const[var]:
126 self.display(3,"Checking",varcon)
127 if varcon.holds(self.current_assignment):
128 if varcon in self.conflicts: #was incons, now consis
129 self.display(3,"Became consistent",varcon)
130 self.conflicts.remove(varcon)
131 for v in varcon.scope: # v is in one fewer
conflicts
132 var_differential[v] =
var_differential.get(v,0)-1
133 else:
134 if varcon not in self.conflicts: # was consis, not now
135 self.display(3,"Became inconsistent",varcon)
136 self.conflicts.add(varcon)
137 for v in varcon.scope: # v is in one more
conflicts
138 var_differential[v] =

https://aipython.org Version 0.9.13 May 7, 2024


100 4. Reasoning with Constraints

var_differential.get(v,0)+1
139 self.variable_pq.update_each_priority(var_differential)
140 self.display(2,"Number of conflicts",len(self.conflicts))
141 if not self.conflicts: # no conflicts, so solution found
142 self.display(1,"Solution found:",
self.current_assignment,"in",
143 self.number_of_steps,"steps")
144 return self.number_of_steps
145 self.display(1,"No solution in",self.number_of_steps,"steps",
146 len(self.conflicts),"conflicts remain")
147 return None

create pq creates an updatable priority queue of the variables, ordered by the


number of conflicts they participate in. The priority queue only includes vari-
ables in conflicts and the value of a variable is the negative of the number of
conflicts the variable is in. This ensures that the priority queue, which picks
the minimum value, picks a variable with the most conflicts.

cspSLS.py — (continued)

149 def create_pq(self):


150 """Create the variable to number-of-conflicts priority queue.
151 This is needed to select the variable in the most conflicts.
152
153 The value of a variable in the priority queue is the negative of the
154 number of conflicts the variable appears in.
155 """
156 self.variable_pq = Updatable_priority_queue()
157 var_to_number_conflicts = {}
158 for con in self.conflicts:
159 for var in con.scope:
160 var_to_number_conflicts[var] =
var_to_number_conflicts.get(var,0)+1
161 for var,num in var_to_number_conflicts.items():
162 if num>0:
163 self.variable_pq.add(var,-num)

cspSLS.py — (continued)

165 def random_choice(st):


166 """selects a random element from set st.
167 It would be more efficient to convert to a tuple or list only once
168 (left as exercise)."""
169 return random.choice(tuple(st))

Exercise 4.15 These implementations always select a value for the variable se-
lected that is different from its current value (if that is possible). Change the code
so that it does not have this restriction (so it can leave the value the same). Would
you expect this code to be faster? Does it work worse (or better)?

https://aipython.org Version 0.9.13 May 7, 2024


4.5. Solving CSPs using Stochastic Local Search 101

4.5.3 Updatable Priority Queues


An updatable priority queue is a priority queue, where key-value pairs can be
stored, and the pair with the smallest key can be found and removed quickly,
and where the values can be updated. This implementation follows the idea
of http://docs.python.org/3.9/library/heapq.html, where the updated ele-
ments are marked as removed. This means that the priority queue can be used
unmodified. However, this might be expensive if changes are more common
than popping (as might happen if the probability of choosing the best is close
to zero).
In this implementation, the equal values are sorted randomly. This is achieved
by having the elements of the heap being [val, rand, elt] triples, where the sec-
ond element is a random number. Note that Python requires this to be a list,
not a tuple, as the tuple cannot be modified.

cspSLS.py — (continued)

171 class Updatable_priority_queue(object):


172 """A priority queue where the values can be updated.
173 Elements with the same value are ordered randomly.
174
175 This code is based on the ideas described in
176 http://docs.python.org/3.3/library/heapq.html
177 It could probably be done more efficiently by
178 shuffling the modified element in the heap.
179 """
180 def __init__(self):
181 self.pq = [] # priority queue of [val,rand,elt] triples
182 self.elt_map = {} # map from elt to [val,rand,elt] triple in pq
183 self.REMOVED = "*removed*" # a string that won't be a legal element
184 self.max_size=0
185
186 def add(self,elt,val):
187 """adds elt to the priority queue with priority=val.
188 """
189 assert val <= 0,val
190 assert elt not in self.elt_map, elt
191 new_triple = [val, random.random(),elt]
192 heapq.heappush(self.pq, new_triple)
193 self.elt_map[elt] = new_triple
194
195 def remove(self,elt):
196 """remove the element from the priority queue"""
197 if elt in self.elt_map:
198 self.elt_map[elt][2] = self.REMOVED
199 del self.elt_map[elt]
200
201 def update_each_priority(self,update_dict):
202 """update values in the priority queue by subtracting the values in
203 update_dict from the priority of those elements in priority queue.

https://aipython.org Version 0.9.13 May 7, 2024


102 4. Reasoning with Constraints

204 """
205 for elt,incr in update_dict.items():
206 if incr != 0:
207 newval = self.elt_map.get(elt,[0])[0] - incr
208 assert newval <= 0, f"{elt}:{newval+incr}-{incr}"
209 self.remove(elt)
210 if newval != 0:
211 self.add(elt,newval)
212
213 def pop(self):
214 """Removes and returns the (elt,value) pair with minimal value.
215 If the priority queue is empty, IndexError is raised.
216 """
217 self.max_size = max(self.max_size, len(self.pq)) # keep statistics
218 triple = heapq.heappop(self.pq)
219 while triple[2] == self.REMOVED:
220 triple = heapq.heappop(self.pq)
221 del self.elt_map[triple[2]]
222 return triple[2], triple[0] # elt, value
223
224 def top(self):
225 """Returns the (elt,value) pair with minimal value, without
removing it.
226 If the priority queue is empty, IndexError is raised.
227 """
228 self.max_size = max(self.max_size, len(self.pq)) # keep statistics
229 triple = self.pq[0]
230 while triple[2] == self.REMOVED:
231 heapq.heappop(self.pq)
232 triple = self.pq[0]
233 return triple[2], triple[0] # elt, value
234
235 def empty(self):
236 """returns True iff the priority queue is empty"""
237 return all(triple[2] == self.REMOVED for triple in self.pq)

4.5.4 Plotting Run-Time Distributions


Runtime distribution uses matplotlib to plot run time distributions. Here the
run time is a misnomer as we are only plotting the number of steps, not the
time. Computing the run time is non-trivial as many of the runs have a very
short run time. To compute the time accurately would require running the
same code, with the same random seed, multiple times to get a good estimate
of the run time. This is left as an exercise.
cspSLS.py — (continued)

239 import matplotlib.pyplot as plt


240 # plt.style.use('grayscale')
241

https://aipython.org Version 0.9.13 May 7, 2024

You might also like