bbqsearch 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bbqsearch/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .bbqsearch import *
bbqsearch/bbqearch.py ADDED
@@ -0,0 +1,580 @@
1
+ #!/usr/bin/env python
2
+ # coding: utf-8
3
+
4
+ # # bbSearch
5
+ #
6
+ # This version of `bbSearch` written during January and February 2022.
7
+ #
8
+ #
9
+ # ### My personal notes:
10
+ # To install on cloud for download use shell script `publish-bbSearch`.
11
+
12
+ # In[47]:
13
+
14
+
15
+ #<<<
16
+ #<<< def __MAKE_SCRIPT__(notebook_stem):
17
+ #<<< get_ipython().system( f' jupyter nbconvert --to python {notebook_stem}.ipynb')
18
+ #<<< with open( notebook_stem + ".py", "r") as f:
19
+ #<<< lines = f.read().split('\n')
20
+ #<<< new_lines = []
21
+ #<<< exclude = False
22
+ #<<< for l in lines:
23
+ #<<< if l.startswith("#<<<"):
24
+ #<<< exclude = True
25
+ #<<< new_lines.append(l)
26
+ #<<< continue
27
+ #<<< if l.startswith("#>>>"):
28
+ #<<< exclude = False
29
+ #<<< new_lines.append(l)
30
+ #<<< continue
31
+ #<<< if exclude:
32
+ #<<< new_lines.append("#<<< " + l)
33
+ #<<< else:
34
+ #<<< new_lines.append(l)
35
+ #<<<
36
+ #<<< new_content = '\n'.join(new_lines)
37
+ #<<< #new_content = "### MODIFIED\n" + new_content
38
+ #<<< with open( notebook_stem + ".py", "w") as f:
39
+ #<<< f.write(new_content)
40
+ #<<<
41
+ #<<< __MAKE_SCRIPT__("bbSearch")
42
+ #<<< get_ipython().system('s3cmd put --acl-public bbSearch.py s3://bb-ai.net/bb-python-modules/')
43
+ #>>>
44
+
45
+
46
+ # In[34]:
47
+
48
+
49
+ from datetime import datetime
50
+ time = datetime.now().strftime("%H:%M, %a %d %b")
51
+ print(f"Loading bbSearch Version 2.1 (at {time})")
52
+ print("Last module source code edit 9am Thursday 24th Feb 2022")
53
+
54
+
55
+ # In[35]:
56
+
57
+
58
+ class SearchProblem:
59
+
60
+ def __init__( self ):
61
+ """
62
+ The __init__ method must set the initial state for the search.
63
+ Arguments could be added to __init__ and used to configure the
64
+ initial state and/or other aspects of a problem instance.
65
+ """
66
+ self.initial_state = None
67
+ raise NotImplementedError
68
+
69
+ def info(self):
70
+ """
71
+ This function is called when the search is stared and should
72
+ print out useful information about the problem.
73
+ """
74
+ print("This is the general SearchProblem parent class")
75
+ print("You must extend this class to encode a particular search problem.")
76
+
77
+ def possible_actions(self, state):
78
+ """
79
+ This takes a state as argument and must return a list of actions.
80
+ Both states and actions can be any kinds of python data type (e.g.
81
+ numbers, strings, tuples or complex objects of any class).
82
+ """
83
+ return []
84
+
85
+ def successor(self, state, action):
86
+ """
87
+ This takes a state and an action and returns the new state that would result
88
+ from doing that action in that state. You can assume that the given action
89
+ is in the list of 'possible_actions' for that state.
90
+ """
91
+ return state
92
+
93
+ def goal_test(self, state):
94
+ """
95
+ This method shoudl return True or False given any state. It should return
96
+ True for all and only those states that are considert "goal" states.
97
+ """
98
+ return False
99
+
100
+ def cost(self, path, state):
101
+ """
102
+ This is an optional method that you only need to define if you are using
103
+ a cost based algorithm such as "uniform cost" or "A*". It should return
104
+ the cost of reaching a given state via a given path.
105
+ If this is not defined, it will is assumed that each action costs one unit
106
+ of effort to perform, so it returns the length of the path.
107
+ """
108
+ return len(path)
109
+
110
+ def heuristic(self, state):
111
+ """
112
+ This is an optional method that should return a heuristic value for any
113
+ state. The value should be an estimate of the remaining cost that will be
114
+ required to reach a goal. For an "admissible" heuristic, the value should
115
+ always be equal to or less than the actual cost.
116
+ """
117
+ raise NotImplementedError
118
+
119
+ def display_action(self, action):
120
+ """
121
+ You can set the way an action will be displayed in outputs.
122
+ """
123
+ print(" ", action)
124
+
125
+ def display_state(self, state):
126
+ """
127
+ You can set the way a state will be displayed in outputs.
128
+ """
129
+ print(state)
130
+
131
+ def display_state_path( self, actions ):
132
+ """
133
+ This defines output of a solution path when a list of actions
134
+ is applied to the initial state. It assumes it is a valid path
135
+ with all actions being possible in the preceeding state.
136
+ You probably don't need to override this.
137
+ """
138
+ s = self.initial_state
139
+ self.display_state(s)
140
+ for a in actions:
141
+ self.display_action(a)
142
+ s = self.successor(s,a)
143
+ self.display_state(s)
144
+
145
+
146
+ # In[36]:
147
+
148
+
149
+ class JugPouringPuzzle( SearchProblem ):
150
+
151
+ def __init__( self, initial_state, goal_quantity ):
152
+ self.initial_state = initial_state
153
+ self.goal_quantity = goal_quantity
154
+ print( "Creating SearchProblem object" )
155
+ print( "Setting initial state to:", self.initial_state )
156
+ print( "Want to measure quantity:", self.goal_quantity )
157
+
158
+ self.jugs = self.initial_state.keys()
159
+
160
+ def info(self):
161
+ print( "\nMeasuring Jugs problem:" )
162
+ print( "You have a number of jugs with a certain volume and initial contents,\n"
163
+ "as follows:")
164
+ for jar in self.initial_state:
165
+ print(f"{jar:>10} : volume={self.initial_state[jar][0]}, "
166
+ f"contents={self.initial_state[jar][1]}")
167
+ print( f"The goal is to measure the quantity {self.goal_quantity} "
168
+ "by pourning liquid between the jugs.")
169
+
170
+ def possible_actions(self, state):
171
+ actions = []
172
+ for j1 in self.jugs:
173
+ for j2 in self.jugs:
174
+ if ( j1!=j2 #different jugs
175
+ and state[j1][1] > 0 # j1 not empty
176
+ and state[j2][0]>state[j2][1] # j2 not full
177
+ ): actions.append((j1,j2))
178
+ return actions
179
+
180
+ def successor(self, state, action):
181
+ new_state = { k:state[k] for k in state }
182
+ j1, j2 = action[0], action[1]
183
+ vol_in_j1 = state[j1][1]
184
+ vol_in_j2 = state[j2][1]
185
+ space_in_j2 = state[j2][0] - state[j2][1]
186
+ if vol_in_j1 <= space_in_j2:
187
+ new_j1 = 0
188
+ new_j2 = vol_in_j1 + vol_in_j2
189
+ else:
190
+ new_j1 = vol_in_j1 - space_in_j2
191
+ new_j2 = state[j2][0]
192
+ new_state[j1] = (state[j1][0], new_j1)
193
+ new_state[j2] = (state[j2][0], new_j2)
194
+ return new_state
195
+
196
+ def goal_test(self, state):
197
+ quantities = [ state[k][1] for k in state]
198
+ return self.goal_quantity in quantities
199
+
200
+ def display_action( self, action ):
201
+ j1, j2 = action
202
+ print(f"Pour from {j1} to {j2}:")
203
+
204
+ def display_state( self, state ):
205
+ jug_quantities = [ f"{k} ({state[k][0]}): {state[k][1]}" for k in state]
206
+ print( ", ".join(jug_quantities))
207
+
208
+
209
+ # In[37]:
210
+
211
+
212
+ import bisect
213
+
214
+ ## This is a Weighted queue optimised by using the bisect module
215
+ ## to insert into the item list (via key indirection).
216
+
217
+ class WeightedQueue:
218
+
219
+ def __init__(self, mode='fifo', weighted=True, weightfunc=None ):
220
+ self.weighted = weighted
221
+ self.weightfunc = weightfunc
222
+ self.mode = mode
223
+ self.weights = []
224
+ self.items = []
225
+
226
+ if mode == 'fifo':
227
+ if self.weighted:
228
+ self.insert = self.insert_right
229
+ self.pop = self.pop_weighted
230
+ else:
231
+ self.insert = self.add_right
232
+
233
+ elif mode == 'lifo':
234
+ if self.weighted:
235
+ self.insert = self.insert_left
236
+ self.pop = self.pop_weighted
237
+ else:
238
+ self.insert = self.add_left
239
+
240
+
241
+ else:
242
+ raise ValueError("!!! WeightedQueue 'mode' keyword arg "
243
+ "must be 'fifo' or 'lifo'")
244
+
245
+
246
+ def add_left(self,item, weight=None):
247
+ self.items.insert(0,item)
248
+
249
+ def add_right(self,item, weight=None):
250
+ self.items.append(item)
251
+
252
+ def insert_left(self, item, weight = None):
253
+ if weight == None:
254
+ weight = self.weightfunc(item)
255
+ ipoint = bisect.bisect_left(self.weights, weight)
256
+ bisect.insort_left(self.weights, weight)
257
+ self.items.insert(ipoint,item)
258
+
259
+ def insert_right(self, item, weight = None):
260
+ if weight == None:
261
+ weight = self.weightfunc(item)
262
+ ipoint = bisect.bisect_right(self.weights, weight)
263
+ bisect.insort_right(self.weights, weight)
264
+ self.items.insert(ipoint,item)
265
+
266
+ def initialise(self, items, weights=None):
267
+ self.items = items
268
+ if weights:
269
+ iwpairs = list(zip(items,weights))
270
+ iwpairs.sort(key=lambda x:x[1])
271
+ self.items = [x[0] for x in iwpairs]
272
+ self.weights = [x[1] for x in iwpairs]
273
+ elif self.weightfunc:
274
+ self.items.sort(key=self.weightfunc)
275
+ self.weights = [self.weightfunc(i) for i in self.items]
276
+
277
+ def pop(self):
278
+ return self.items.pop(0)
279
+
280
+ def pop_weighted(self):
281
+ self.weights.pop(0)
282
+ return self.items.pop(0)
283
+
284
+ def display(self):
285
+ for w, i in zip(self.weights, self.items):
286
+ print(f"{w}: {i}")
287
+
288
+
289
+
290
+ # In[44]:
291
+
292
+
293
+ class SearchQueue:
294
+
295
+ def __init__( self, mode='BF/FIFO', cost=None, heuristic=None ):
296
+ self.mod = mode
297
+ self.cost = cost
298
+ self.heuristic = heuristic
299
+ self.weighted = (cost!=None or heuristic!=None)
300
+
301
+ #mode parameter determines whether queue mode is filo or fifo
302
+ modemap = { 'BF/FIFO':'fifo', 'DF/LIFO': 'lifo',
303
+ 'bf' : 'fifo', 'df': 'lifo',
304
+ 'BF' : 'fifo', 'DF': 'lifo',
305
+ 'fifo': 'fifo', 'lifo': 'lifo',
306
+ 'FIFO': 'fifo', 'LIFO': 'lifo',
307
+ 'breadth_first': 'fifo', 'depth_first': 'lifo',
308
+ }
309
+ if not mode in modemap.keys():
310
+ raise ValueError("!!! SearchQueue 'mode' argument "
311
+ "must be 'BF/FIFO' or 'DF/LIFO'")
312
+
313
+ self.wq = WeightedQueue(modemap[mode], weighted=self.weighted)
314
+
315
+ def empty(self):
316
+ return self.wq.items == []
317
+
318
+ def len(self):
319
+ return len(self.wq.items)
320
+
321
+ def insert(self,item, weight=None):
322
+ # print("Insert in SearchQueue")
323
+ # print("item=", item)
324
+ # print("weight=", weight)
325
+ self.wq.insert(item, weight=weight)
326
+
327
+ def initialise(self,items,weights=None):
328
+ return self.wq.initialise(items,weights=weights)
329
+
330
+ def pop(self):
331
+ return self.wq.pop()
332
+
333
+
334
+
335
+
336
+ #sq = SearchQueue()
337
+
338
+
339
+ # In[45]:
340
+
341
+
342
+ import time, random
343
+
344
+ def search( problem,
345
+ mode,
346
+ max_nodes,
347
+ loop_check = False,
348
+ randomise = False,
349
+ cost = None,
350
+ heuristic = None,
351
+ show_path = True,
352
+ show_state_path = False,
353
+ dots = True,
354
+ return_info = False,
355
+ #One could potentially define other node limits
356
+ #max_generated = False,
357
+ #max_discarded = False
358
+ ):
359
+
360
+ problem.info()
361
+
362
+ print( "\n** Running Brandon's Search Algorithm **")
363
+ cost_name = (cost.__name__ if cost else None)
364
+ heuristic_name = (heuristic.__name__ if heuristic else None)
365
+ print( f"Strategy: mode={mode}, cost={cost_name}, heuristic={heuristic_name}")
366
+ print( f"Max search nodes: {max_nodes} (max number added to queue)" )
367
+
368
+ start_time = time.perf_counter()
369
+ queue = SearchQueue(mode,cost,heuristic)
370
+ queue.initialise( [([], problem.initial_state )], weights=[0] )
371
+ global weight_function
372
+ weight_function = node_weight_function( cost, heuristic )
373
+
374
+ states_seen = {problem.initial_state.__repr__()}
375
+ nodes_generated = 1 # counting initial state
376
+ nodes_kept = 1
377
+ nodes_tested = 0
378
+ nodes_discarded = 0
379
+
380
+ termination_condition = None
381
+
382
+ node_limit_exceeded = False
383
+
384
+ if not dots:
385
+ print( "Search started (progress dot output off)", flush=True)
386
+
387
+ while True:
388
+ if queue.empty():
389
+ termination_condition = "SEARCH-SPACE_EXHAUSTED" # Means there is no solution.
390
+ break
391
+
392
+ if dots:
393
+ if nodes_tested % 1000 == 0:
394
+ if nodes_tested==0:
395
+ print("Searching (will output '.' each 1000 goal_tests)", flush=True)
396
+ else:
397
+ print('.', end='')
398
+ if nodes_tested % 100000 == 0:
399
+ print( f' ({nodes_tested})', flush=True)
400
+
401
+ path, state = queue.pop()
402
+ nodes_tested += 1
403
+ if problem.goal_test(state):
404
+ termination_condition = "GOAL_STATE_FOUND"
405
+ break
406
+
407
+ if node_limit_exceeded:
408
+ termination_condition = "NODE_LIMIT_EXCEEDED"
409
+ break
410
+
411
+ #print("Considering state:", state)
412
+ actions = problem.possible_actions(state)
413
+ #print("Possible actions are:", actions)
414
+
415
+ if randomise: # randomise action choice sequance (may be useful in DFS)
416
+ random.shuffle(actions)
417
+
418
+ for i, a in enumerate(actions):
419
+ suc = problem.successor(state,a)
420
+ nodes_generated += 1
421
+ if loop_check:
422
+ if suc.__repr__() in states_seen:
423
+ nodes_discarded += 1
424
+ continue # skip already seen state
425
+ else:
426
+ states_seen.add(suc.__repr__())
427
+ nodes_kept += 1
428
+ if nodes_kept > max_nodes:
429
+ node_limit_exceeded = True
430
+ break
431
+ extended_path = path+[a]
432
+ #print("Making node with path:", extended_path)
433
+ weight=weight_function(extended_path,suc)
434
+ #print("weight=", weight)
435
+ queue.insert((extended_path,suc),
436
+ weight=weight)
437
+
438
+ if termination_condition == "GOAL_STATE_FOUND":
439
+ print( "\n:-)) *SUCCESS* ((-:\n" )
440
+ print( f"Path length = {len(path)}" )
441
+ print( "Goal state is:")
442
+ problem.display_state(state)
443
+ if cost != None:
444
+ print("Cost of reaching goal:", cost(path,state))
445
+ if show_path:
446
+ print( "The action path to the solution is:" )
447
+ for a in path:
448
+ problem.display_action(a)
449
+ print()
450
+ if show_state_path:
451
+ print( "The state/action path to the solution is:" )
452
+ problem.display_state_path(path)
453
+ goal_state = state
454
+ path_length = len(path)
455
+
456
+ if termination_condition == "SEARCH-SPACE_EXHAUSTED":
457
+ print("\n!! Search space exhausted (tried everying) !!")
458
+ print("): No solution found :(\n")
459
+ goal_state=path=path_length=None
460
+
461
+ if termination_condition == "NODE_LIMIT_EXCEEDED":
462
+ print( f"\n!! Search node limit ({max_nodes}) reached !!")
463
+ print("): No solution found :(\n")
464
+ goal_state=path=path_length=None
465
+
466
+ print( f"\nSEARCH SPACE STATS:")
467
+ print( f"Total nodes generated = {nodes_generated:>8} (includes start)")
468
+
469
+ if loop_check:
470
+ distinct_states_seen = len(states_seen)
471
+ else:
472
+ distinct_states_seen = "not recorded (loop_check=False)"
473
+
474
+ if loop_check:
475
+ print( f"Nodes discarded by loop_check = {nodes_discarded:>8}"
476
+ f" ({nodes_generated-nodes_discarded} distinct states added to queue)" )
477
+ #print( f"Distinct states seen = {distinct_states_seen:>8}" )
478
+
479
+ print( f"Nodes tested (by goal_test) = {nodes_tested:>8}",end=' ' )
480
+ if termination_condition == "GOAL_STATE_FOUND":
481
+ print(f" ({nodes_tested-1} expanded + 1 goal)")
482
+ else:
483
+ print( " (all expanded)")
484
+
485
+ print( f"Nodes left in queue = {queue.len():>8}")
486
+ time_taken = time.perf_counter() - start_time
487
+ print( f"\nTime taken = {round(time_taken, 4)} seconds\n" )
488
+
489
+ if not return_info:
490
+ return termination_condition
491
+ else:
492
+ return {
493
+ "args" : {"problem" : problem.__class__.__name__,
494
+ "mode" : mode,
495
+ "max_nodes" : max_nodes,
496
+ "loop_check" : loop_check,
497
+ "randomise" : randomise,
498
+ "cost" : cost_name,
499
+ "heuristic" : heuristic_name,
500
+ "dots" : dots,
501
+ },
502
+ "result": {
503
+ "termination_condition": termination_condition,
504
+ "goal_state" : goal_state,
505
+ "path" : path,
506
+ "path_length" : path_length,
507
+ },
508
+ "search_stats" : {
509
+ "nodes_generated" : nodes_generated,
510
+ "nodes_tested" : nodes_tested,
511
+ "nodes_discarded" : nodes_discarded,
512
+ "distinct_states_seen" : distinct_states_seen,
513
+ "nodes_left_in_queue" : queue.len(),
514
+ "time_taken" : time_taken,
515
+ }
516
+ }
517
+
518
+
519
+
520
+ def node_weight_function( cost, heuristic ):
521
+ if not cost and not heuristic:
522
+ return lambda p,s: None
523
+ if cost and (not heuristic):
524
+ return cost
525
+ if (not cost) and heuristic:
526
+ return lambda p,s: (heuristic(s))
527
+ if cost and heuristic:
528
+ return lambda p,s:(cost(p,s)+heuristic(s))
529
+
530
+
531
+
532
+
533
+ # In[46]:
534
+
535
+
536
+ def thecost(p,s):
537
+ return(len(p)**2)
538
+
539
+
540
+ def test():
541
+ JPP1 = JugPouringPuzzle(
542
+ {"small":(3,0), "medium":(5,0), "large":(8,8)}, #initial state
543
+ 4 # goal measurement
544
+ )
545
+
546
+ JPP2 = JugPouringPuzzle(
547
+ {"small":(3,3), "medium":(5,0), "large":(8,8)}, #initial state
548
+ 12 # goal measurement
549
+ )
550
+
551
+
552
+
553
+ res1 = search( JPP1, 'BF/FIFO', 10000, cost=thecost, loop_check=False, show_state_path=True )
554
+ print("res1 ="); display(res1)
555
+ res2 = search( JPP2, 'DF/LIFO', 10000,
556
+ #dots = False,
557
+ randomise=True, loop_check=False, return_info=True)
558
+ print("res2 ="); display(res2)
559
+
560
+ if __name__ == "__main__":
561
+ test()
562
+
563
+
564
+ # In[ ]:
565
+
566
+
567
+
568
+
569
+
570
+ # In[ ]:
571
+
572
+
573
+
574
+
575
+
576
+ # In[ ]:
577
+
578
+
579
+
580
+
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: bbqsearch
3
+ Version: 0.1.0
4
+ Summary: General configurable queue-based search algorithm for teaching AI.
5
+ Author-email: Brandon Bennett <B.Bennett@leeds.ac.uk>
6
+ License: MIT
7
+ Keywords: search,teaching
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+
11
+ # bbqsearch
12
+
13
+ `bbqsearch` is a simple, configurable, queue-based search algorithm intended for AI teaching.
14
+
15
+ **Status:** Experimental research and teaching tool.
16
+
17
+ The package is designed primarily for use in interactive environments such as a Jupyter notebook
18
+ or in Google Colab, providing a simple interface for running Prover9 from Python
19
+ code and capturing the resulting proofs and diagnostic output.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install bbqsearch
25
+ ```
@@ -0,0 +1,5 @@
1
+ bbqsearch/__init__.py,sha256=IQ9LMYj-kIuvTrU9K8TC3SWBI5eAYjLOR5N2O2XuHHA,25
2
+ bbqsearch/bbqearch.py,sha256=7yNMlLjoS7AaVQGxc8nB4pidgA2xf6CsVWG-2hpuQaM,18767
3
+ bbqsearch-0.1.0.dist-info/METADATA,sha256=tbIhiP1eU3L6a6T_xZAHrR_IOagaAeQnb5dMM1WKLkg,742
4
+ bbqsearch-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ bbqsearch-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any