class: center, middle # Artificial Intelligence ### Lab 1 --- class: center, middle # Lab 1: Pathfinding --- class: mmedium # For the Lab * In Lab 1 you will implement Breadth-First Search, Depth-First Search, Greedy Search and A* for arbitrary graphs * You will get a python "framework" that contains some example graphs you can test your algorithms on * You should also come up with at least one graph of your own (a map of your home town, campus, your favorite video game, ...) * You can work in groups of up to two students * The submission deadline is on Tuesday, 3/2, AoE --- # The "framework" * First step: Download [pathfinding.zip](https://yawgmoth.github.io/CS4200/assets/pathfinding.zip) * It contains two files: - `pathfinding.py`: Where you should implement the search algorithms - `graph.py`: Definition of graphs, with two sample graphs * You can solve the entire lab by only editing `pathfinding.py`! --- # Running the code * `python pathfinding.py` will run some test cases (look for `main`) * As-is none of the pathfinding algorithms are implemented * The output will therefore just consist of "No path found" * You will have to implement the algorithms using the given function definitions! * In other words: **Do not change the API** --- # What you have to do There are four functions you have to implement: ```python bfs(start, goal) dfs(start, goal) greedy(start, heuristic, goal) astar(start, heuristic, goal) ``` `start` is a Node object, `heuristic` and `goal` are both functions (!) * `heuristic` is a function that takes a node object as a parameter and returns a `float` value * `goal` is a function that takes a node object as a parameter and returns a `bool` value --- # Example call ```python target = "Bregenz" def atheuristic(n): # Node -> float return graph.AustriaHeuristic[target][n.get_id()] def atgoal(n): # Node -> bool return n.get_id() == target result = astar(graph.Austria["Eisenstadt"], atheuristic, atgoal) ``` We will talk about this `Austria` graph in a bit. --- # Nodes and Edges ```python class Node: def get_neighbors(self): # returns a list of edges to neighbors return [] def get_id(self): # returns a unique identifier for the node return "" # Each edge contains the target node, the cost of taking # that edge and a name (for printing the path) class Edge: def __init__(self, target, cost, name): self.target = target self.cost = cost self.name = name ``` Note: `Node` is the **base class**. Actual subclasses will return a non-empty list of neighbors and different IDs. --- # Expected Result Each of the search algorithms should return four values as a tuple: * A list of Edge objects that represent the path * The total length of that path * The number of nodes that were visited/seen by the algorithm (added to the frontier) * The number of nodes that were expanded by the algorithm (removed from the frontier) --- # An Example: Random Search ```python def randomsearch(start, heuristic, goal): current = start path = [] expanded, visited, length = 0,0,0 while not goal(current): # this is how you would get a heuristic value h = heuristic(current) neighbors = current.get_neighbors() # neighbors: [Edge] selection = random.choice(neighbors) path.append(selection) length += selection.cost expanded += 1 visited += len(neighbors) current = selection.target return path,length, visited, expanded ``` --- # Random Search is Bad! ``` visited nodes: 60 expanded nodes: 17 Path found with cost 2516.0 Eisenstadt - Graz Graz - Vienna Vienna - Graz Graz - Eisenstadt Eisenstadt - Vienna Vienna - Graz Graz - Bruck Bruck - Vienna Vienna - Linz Linz - Vienna Vienna - Eisenstadt Eisenstadt - Vienna Vienna - Bruck Bruck - Klagenfurt Klagenfurt - Lienz Lienz - Innsbruck Innsbruck - Bregenz ``` --- # The Austria Graph
--- # Graphs * Take a look at `graph.py` to see how these classes are used to set up a finite graph (of Austria), as well as an infinite graph. * You do not have to change anything in `graph.py`, but it is useful to create more test cases * You actually **have to** come up with one new graph as an extra test case * Making more is fine (and it will help you detect bugs!) --- # Report In addition to the code, you also have to submit a short report (no minimum length): * How did you implement the algorithms? * Performance of the four algorithms on the given test cases * Your own test graph(s) and observations about the algorithm performance --- class: medium # Common Pitfalls - Try a test case where there is no path (e.g. try finding a path to "Narnia" on the Austria-graph) - When you do A\*, you **have to**: * When you try to add a node to the frontier, check if it is already there and compare costs * When you "reach" the goal, make sure that there is no lower-cost option still left in the frontier - Make sure you answer the questions in the report (including which algorithm performs better on which graphs) --- class: center, middle # Python --- # Python * The lab is in python, as are the others * You don't need to learn "all" of python * This lab, in particular, is great to learn the basics * Here's a short overview of what you need --- # Python Programs * Python is a scripting language * This means you can just write a sequence of instructions into a file and run it * Best practice: Define a function `main`, and call that (only) if the script is run directly * You can `import` other scripts to access their function and class definitions --- # Hello World ```python import sys def main(who): print("Hello", who) if __name__ == "__main__": if len(sys.argv) > 1: main(sys.argv[1]) else: main("world") ``` --- # Running it: If you save this as `helloworld.py` you can * Run it with `python helloworld.py` and get `Hello world` * Run it with `python helloworld.py CS4200` and get `Hello CS4200` * `import helloworld` in another python script, and then call `helloworld.main("from the other side")` and get `Hello from the other side` --- # UGH, Command Line * Your favorite text editor/IDE likely has some python integration * I know VS Code and Atom do; personally, I use Notepad++ and the command line * Unless you know what you are doing, I do **not** recommend Anaconda * Command line **always** works, even when we need pytorch later in the semester --- # Data Structures The most important data structures in Python (for now): * Lists: `[1,2,3,4,5]` * Dictionaries: `{1: "one", 2: "two", 7: "seven"}` * Sets: `{1, 2, 3, 4}` * Tuples: `(1, 2, 3, 4)` --- class: medium # Lists * Lists are the most ubiquitous of the four * Unless you have specific requirements, use a list * `mylist.append(x)`: Add an element to the end * `mylist[0]`: Get the first element * `mylist[-1]`: Get the last element * `mylist[3:5]`: Get a sublist of length 2, starting with the 4th element (element at index 3) --- # Lists as Stacks * Push: `mystack.append(x)` * Pop: `x = mystack.pop(-1)` (-1: last element) * In some cases it may be useful to split up getting and removing up: ```python x = mystack[-1] # Do something else del mystack[-1] ``` --- # Lists as Queues * Put: `myqueue.append(x)` * Get: `x = myqueue.pop(0)` (0: first element) * Similarly, you can split it up ```python x = myqueue[0] # Do something else myqueue = myqueue[1:] ``` --- # Dictionaries * Dictionaries are great if you want to **map** something * For example: Which edge did you follow to come to a node * `mydict[key] = value`, where key can be anything **immutable** (importantly: not a list) * `value = mydict[key]` get a value (exception if `key` does not exist in mydict) * `value = mydict.get(key, default)` get a value with a default if `key` does not exist --- # Sets * Sets are like unordered lists, so you can not get the "first" element * Great (fast!) if you just want to keep track of something (like, which nodes you have expanded) * `myset.add(x)`: add `x` to the set * `x in myset`: Check if `x` is in the set * `myset.remove(x)`: Remove `x` from set (exception if it is not present) --- # Tuples * Like lists, but once created you can not modify them: `x = (1,2,3,4)` * Useful: Can be used as keys of dictionaries * That's also what you get if you `return a,b,c,d` from a function * Also works in the other direction: `a,b,c,d = x` --- # Iteration You can iterate over any of these sequences: ```python for item in sequence: print(item) ``` * For lists and tuples you will get the items in order * For sets you get them in **arbitrary** order * For dictionaries you get the **keys** in "arbitrary" order --- # Classes and Objects Class definition: ```python class Edge: def __init__(self, target, cost, name): self.target = target self.cost = cost self.name = name ``` There is no separate definition of "fields"; just like variables, you define them anywhere (but usually this is done in `__init__`) ```python myedge = Edge(somenode, 123, "demo edge") myedge.extra_info = "You can add anything" ``` --- # Functions Functions are objects, too! (almost everything is an object) ```python def atgoal(n): return n.get_id() == target # you can assign it to another variable x = atgoal # you can add fields x.extra_info = "the goal function" # you can pass it as a parameter to other functions allgoals = map(atgoal, allnodes) ``` --- # Wait, what? Who is `map`? ```python allgoals = map(atgoal, allnodes) ``` `map`: Apply a function to all items in a sequence (like a list) ```python def f(n): return n*n squares = map(f, [1,2,3,4]) for value in squares: print(value) ``` will print 1, 4, 9, and 16 --- # Sorting Lists * Lists have a `sort` method. * It has some interesting/relevant parameters! * `key` allows you to pass a function that calculates a value that should be compared (instead of the actual entry in the list) * `reverse` allows you to sort descending instead of ascending --- # Sorting Keys * You have a list of nodes * There is a heuristic function Sort by heuristic: ```python nodes.sort(key=heuristic) ``` This will call heuristic for every node, and use the result of that function to determine the ordering! --- # The Zen of Python, by Tim Peters ```python >>> import this Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Readability counts. Special cases are not special enough to break the rules. Errors should never pass silently. There should be only one obvious way to do it. Namespaces are one honking great idea -- let's do more of those! ``` (Abbreviated version) --- # And don't forget: Whitespace matters ``` >>> from __future__ import braces File "
", line 1 SyntaxError: not a chance ``` * This is, IMO, the least intrusive feature of python * Write your code like you should anyway, and it will work * Just don't mix tabs and spaces (my code uses 4 spaces) --- # References * [Official Python Tutorial](https://docs.python.org/3/tutorial/index.html) * [Python for Java programmers](http://python4java.necaiseweb.org/) * [Python Cheatsheet by Kate Compton](https://github.com/galaxykate/PythonCheatsheet/blob/main/pythonCheatsheet.md) * [Does Visual Studio Rot the Mind](http://charlespetzold.com/etc/DoesVisualStudioRotTheMind.html)