diff --git a/RouteFinding/05_tests.py b/RouteFinding/05_tests.py new file mode 100644 index 0000000..edc5539 --- /dev/null +++ b/RouteFinding/05_tests.py @@ -0,0 +1,34 @@ +# Define Searcher, hSLD, SearchNode. + +# (b) Show your program loading in the 30-node sample file. +s=Searcher("30node.txt") +# (c) Show you program setting start node=U and end node=T. +s.setStartGoal('U','T') +# myViz should be a DRDViz instance -> save map to file on disk. +s.myViz.save("30node.png") +# (d) Show the one open node. +[n.showBasic() for n in s.open] +# (e) Show successors of only open node. +initial_children = s.successors(s.open.pop(0)) +[n.showBasic() for n in initial_children] + +# (f) Show three inserts: at the front, and the end, and "in order" +def reset_insert(where): + s.reset() + initial_children = s.successors(s.open.pop(0)) + insert_method = getattr(s, "insert_"+where) + insert_method(initial_children) + return [n.showBasic() for n in s.open] + +reset_insert("front") +reset_insert("end") +reset_insert("ordered") + +# (g) INSERT (K,500), (C,91) and (J,10) and show no duplicates. +newdata = (("K",500), ("C",91), ("J",10)) +newlist = [SearchNode(label=label, pathcost=pathcost) for label, pathcost in newdata] +ignored = s.insert_end(newlist) +[n.showBasic() for n in s.open] + +# 3. hSLD heuritic function being called on three nodes. +[hSLD(x, s) for x in ("V", "AC", "J")] diff --git a/RouteFinding/DRDViz.py b/RouteFinding/DRDViz.py new file mode 100644 index 0000000..23bcde0 --- /dev/null +++ b/RouteFinding/DRDViz.py @@ -0,0 +1,245 @@ +# DRDViz is a powerful graph visualizer graph specifically for +# visualizing searches in a given search space. It it designed to be +# used an an "attached helper" to some sort of graph searching class. +# The overall idea is that you you create an instance of your graph +# searcher object, loading some map file. You then create yourself an +# instance of the DRDViz class, and load it with the same map file as +# your search class; the new DRDViz is stored in an instance variable +# of your graph searcher object. Each time your graph searcher does +# something significant e.g. setting start/edge node, exploring a new +# node in the search, or adding new siblings to the OPEN list of the +# search, it simply calls a method on the attached DRDViz to visually +# show the evolving search on the graphical map. + +## Usage examples: +# x=DRDViz() # make yourself a new visualizer +# x.loadGraphFromFile('testfile.txt') # load it up from a map file +# x.plot() # make the plot of the road map appear +# x.markStart('A') # mark starting node A +# x.markGoal('D') # mark 'D' as a goal node +# x.exploreNode('J',['A','J']) # mark node 'J' as being currently explored, along with the path to it from A +# x.exploreEdges('G',['C','B']) # mark the edges coming from parent 'G' and going to children [C, B]. +# +# x.reset() # reset the visualizer, e.g., in preparation for a new search. +# +# ## + +__author__ = "Eck Doerry" +__copyright__ = "Copyright 2018, Northern Arizona University, Flagstaff AZ" + +import numpy as np +import scipy.spatial as graph +import matplotlib.pyplot as plt +from node import Node +from edge import Edge + + +### GRAPHVIZ +# This class implements a graph visualizer. It can plot a graph, then provides methods to mark start/end nodes, and to +# color/re-color nodes and edges in various ways to visualize exploration of the space defined by the graph. +class DRDViz: + + # to create a GraphVisualizer, you first create the blank visualizer (no args taken by INIT). + # You could then use either loadGraphFromFile() or loadGraphFromObjects() to suck in a map to visualize + # and create internal data structures for use in all other operations. + def __init__(self): + self.nodes=[] + self.edges=[] + self.nodeIndex={} + self.markedNodes=[] + self.markedEdges=[] + + +###### EXTERNAL INTERFACE METHODS ############ +# The primary methods you call to use this visualizer + + # Just resets the visualizer. Easiest to just close the graph and redraw it + def reset(self): + self.markedEdges = [] + self.markedNodes = [] + plt.close() + self.plot() + + def save(self, out): + plt.savefig(out) + + # A function to load up a graph to visualize. The filename specified by "infile" should contain a well-formed + # input file describing the graph. The format is a set of edge descriptions, one line per edge, of following format: + # (nodeLabe1, nodeLabel2, edgeLabel, [x1,y1],[x2,y2],[midx,midy]) + # Reads each line and creates the appropriate node/edge objects. + def loadGraphFromFile(self, infile): + with open(infile) as f: + lines = f.readlines() + cleanlines = [x.strip() for x in lines] #strip off any weird leading or trailing spaces. + for line in cleanlines: + line=line.replace('\'','').replace('[','').replace(']','').replace(' ','').strip('()') + rawEdge=line.split(',') # now just have nice clean comma-separated values. Make it a list + [l1,l2,label,x1,y1,x2,y2]=rawEdge # grab all the components of the edge + if l1 not in self.nodeIndex: + self.nodeIndex[l1]=(int(x1),int(y1)) + self.nodes.append(Node(int(x1),int(y1),l1)) + if l2 not in self.nodeIndex: + self.nodeIndex[l2] = (int(x2), int(y2)) + self.nodes.append(Node(int(x2), int(y2), l2)) + # Now build and Edge object for it + newEdge=Edge((int(x1),int(y1)), (int(x2),int(y2)), int(label)) + newEdge.setLabels(l1,l2) + self.edges.append(newEdge) + f.close() + + # Function to ask a loaded graph to plot itself. If you're not in interactive Python mode, may need to call + # plot.show() to see it. Assumes that a graph has been loaded first! + def plot(self): + plt.close() # just in case previous graphs were open! + plt.ion # turn on interactive mode. Not sure if it's critical + # first plot the nodes + plt.plot([x.x for x in self.nodes], [x.y for x in self.nodes], 'ko', + color="#BBBCBD") # plot vertices circles first + for node in self.nodes: # now plot the labels + plt.text(node.x, node.y, node.label, color='b', weight='normal') + # Now plot in the edges + for edge in self.edges: + plt.plot([edge.x1, edge.x2], [edge.y1, edge.y2], 'b-', color='k', linewidth=0.5) # make the lines + plt.text(edge.midx, edge.midy, edge.label, size='x-small') # label the edge at its midpoint + self.paintGraph() + + + # Takes in start node label, and repaints that node as start (ie, different node shape and green) + start_color = 'c' #cyan https://matplotlib.org/stable/tutorials/colors/colors.html + def markStart(self,startLabel): + #self.paintNode(startLabel, self.start_color, weight='bold') # paint label of start node + self.plotVertex(startLabel, 'D', self.start_color) # give start node a vertex + self.paintGraph() + + # Takes in end node label, and repaints that node as end (ie, different node shape and red) + def markGoal(self,goalLabel): + #self.paintNode(goalLabel, 'r', weight='bold') # paint label of endnode red + self.plotVertex(goalLabel, 'D', 'r') # give end node a different label shape/color + self.paintGraph() # force a repaint + + + # ExploreNode is a major method, called each time a new node is opened for exploration. + # takes in: the label of the next node to explore, and the path (list of nodelabels) to that node. + # it uses the latter to highlight the path leading to that node on the graph...could pass it an empty list + # if you don't want that function. + # First it "cleans up" the graph from the last round of exploration: it unmarks the parent node, + # and also unmarked all edges painted in the previous round by exploreEdges() and paintPath() + # It then resets self.paintedEdges and is ready to start the new cycle by painting the node being + # explored. + def exploreNode(self,nodeLabel,nodePath): + # First clean up. Unmark highlights nodes/edges from last step + for edge in self.markedEdges: # first unmark any edges painted in last steps + self.paintEdge(edge[0],edge[1],color='k') + if self.markedNodes: # if there was a previous node explored, unmark it + self.paintNode(self.markedNodes[0], color='k') + self.markedEdges = [] + # Ok, now highlight the newly explored node and its path + self.paintNode(nodeLabel, color='r') + self.paintPath(nodePath, color='r') # mark path to current node in red + self.markedNodes= [nodeLabel] # add node to explored nodes list + self.paintGraph() # force a repaint + + + # ExploreEdges extends the explored frontier out from some parent node to its children. + # take in a parent node label, and a set of children node labels. + # it first paints all previously explored edges a neutral color, then paints the new + # edges to highlight them. + def exploreEdges(self,parentLabel, childLabelList): + for newDest in childLabelList: + #print("Painting: "+parentLabel+" to "+newDest) + self.markedEdges += [(parentLabel,newDest)] # add the new edge to explored edges + self.paintEdge(parentLabel,newDest,color='m') # paint edge to highlight + self.paintNode(newDest,color='g') # color new dest node as part of frontier + self.paintGraph() # to redraw the graph! + + # paintPath takes in a contiguous path of node labels and paints it (default=red). + def paintPath(self, nodePath, color='r'): + last = "" + for node in nodePath: + if last: + self.paintEdge(last, node, color=color) + self.markedEdges += [(last, node)] + last = node + else: + last = node + + +############ INTERNAL UTILITY METHODS ###################### +## Don't really need to call these to use DRDViz. These methods are called by the External Interface +## methods listed above to get the job done. +## Also include various little methods useful for debugging or other purposes. + + # Takes in a node label and color. If node exists, paint its label that color + def paintNode(self,nodeLabel,color='r',weight='normal', size='medium'): + p1=self.nodeLoc(nodeLabel) + if p1: + plt.text(p1[0], p1[1], nodeLabel, color=color, size=size, weight=weight) # repaint the label! + return 1 + else: + print("PaintNode: No node labeled "+nodeLabel+" exists.") + return 0 + + # Load a graph given a list of nodes object and a list of edge objects. Useful convenience when used in + # conjunction with a GraphMaker, allow you to just grab the Edge objects directly from the GraphMaker, rather + # than exporting them to a file first and then reloading them with loadGraphFromFile() + def loadGraphFromObjects(self, nodes, edges): + self.nodes = nodes + self.edges = edges + + # Useful if some outside object wants to get a list of edges in the visualization. + # Just returns a big list of edges in form (endlabel,endlabel, edgelabel,[x1,y1],[x2,y2]) + def getSearchSpace(self): + result = [] + for edge in self.edges: + result.append((edge.endlabel1, edge.endlabel2, edge.label, edge.p1, edge.p2)) + return result + + # Just returns a graphed node object based on its label + def getNodeByLabel(self, nodeLabel): + for node in self.nodes: + if node.label == nodeLabel: return node + print("getNodebyLabel: No node labeled " + nodeLabel + " exists") + return 0 + + # a function that repaints (replots) an edge on the graphical plot + # take in to node labels and optionally, a color + def paintEdge(self, startLabel, endLabel, color='r'): + p1 = self.nodeLoc(startLabel) + p2 = self.nodeLoc(endLabel) + plt.plot([p1[0], p2[0]], [p1[1], p2[1]], color=color, linewidth=1) # repaint the edge + + ## Main External interface functions for using a RoadGraph + def paintGraph(self): + plt.pause(0.05) + + # Job is to (re)plot a node vertex in different shape/color. Takes in node label, a new vertex shape code, + # and a new color, and redraws the vertex associated with that node accordingly. + def plotVertex(self,nodeLabel,mtype,color): + loc= self.nodeLoc(nodeLabel) + plt.plot(loc[0],loc[1],color+mtype) + + + + + def nodeLoc(self,label): # finds and returns the location of node labeled 'label' + for node in self.nodes: + if node.label==label: return [node.x,node.y] + print("nodeLoc: No node with label "+label+" exists") + + + #mainly for debugging: Functions to have the graph display its edges and nodes + def show(self): + print("Here is the graph:") + print("Nodes: ") + print([x.get() for x in self.nodes]) + print("Edges: ") + print([x.get() for x in self.edges]) + + def die(self): # just kills any open graph + plt.close() + +if __name__ == "__main__": + x=DRDViz() + x.loadGraphFromFile("test1.txt") + +DRDViz_class = DRDViz diff --git a/RouteFinding/edge.py b/RouteFinding/edge.py new file mode 100644 index 0000000..7e2bd0b --- /dev/null +++ b/RouteFinding/edge.py @@ -0,0 +1,59 @@ +## The EDGE class is just a utility class for the GraphViz and GraphMaker classes. It simply represents an edge in +# a graph being constructed, capturing the endpoints of the edge as well as its label. Its biggest active +# contribution is that it is able to compute the edge label based on some distortion percentage (between 0-1) that is +# passed into the constructor as the "label". + +__author__ = "Eck Doerry" +__copyright__ = "Copyright 2018, Northern Arizona University, Flagstaff AZ" + +import numpy as np +import scipy.spatial as graph +import matplotlib.pyplot as plt + +class Edge: + + # Makes a new edge. You pass in two points (x,y tuples) plus a label. Label is used flexibly to determine + # how the edge is labeled. If label=0, then edge is just labeled with exact straightline distance between + # the points. If label is a value between 0-1, this is viewed as a percentage to distort the distance by. + # If label > 1, then is taken as an actual value that you want placed on that edge. + def __init__(self, p1,p2, label=0): + self.p1=p1 + self.p2=p2 + self.x1=p1[0] + self.y1=p1[1] + self.x2=p2[0] + self.y2=p2[1] + self.dist= int(graph.distance.euclidean(p1,p2)) # holds straight cartesian dist between points! + self.midx = abs(p1[0] - p2[0]) / 2 + min(p1[0], p2[0]) + self.midy = abs(p1[1] - p2[1]) / 2 + min(p1[1], p2[1]) + self.endlabel1="" + self.endlabel2="" + self.label=0 # holds the actual edge value of the edge. Calc'd by distorting the distance + + # if a distortion was passed in, randomly distort edge length appropriately. + if label==0: self.label=self.dist # default. Edge label is its length + elif label<1: # label was a distortion percentage. randomly calc length based on it. + adjust=self.dist*label + distortion= np.random.random()*adjust # randomly take some percentage of the adjustment range + self.label = int(self.dist + distortion) # adds on distortion. Roads are ALWAYS longer than SLD + else: self.label=label # label was an actual number you want on the edge + + def get(self): + return ([self.x1,self.y1],[self.x2,self.y2],self.label,[self.midx,self.midy]) + + def midpt(self): + return([self.midx,self.midy]) + + # Returns true if the edge connects the two labels. Have to check both directions! + def connects(self,label1,label2): + if (label1 == self.endlabel1 and label2 == self.endlabel2) or (label1 == self.endlabel2 and label2 == self.endlabel1): + return True + return False + + def setLabels(self,l1,l2): # just stored the node labels at the ends of the edge. For convenience mostly. + self.endlabel1=l1 + self.endlabel2=l2 + +# Unit test main fn +# x= Edge((23,56),(34,45)) +# print("created an edge: "+ str(x.get())) diff --git a/RouteFinding/graphmaker.py b/RouteFinding/graphmaker.py new file mode 100644 index 0000000..21472b8 --- /dev/null +++ b/RouteFinding/graphmaker.py @@ -0,0 +1,265 @@ + +## INTRO. Purpose of the GraphMaker class is to generate random graphs; these graphs can then also be edited, adding +# or deleting edges while visually inspecting the evolving map. This makes it quick and easy to create custom "road +# networks" to use as search spaces for developing and visualizing the behavior of searching algorithms. +# +# You ask it to create a graph, you can optionally ask it to +# plot that graph for you for visual verification purposes. You can then edit the graph as desired, quickly adding +# or deleting edge. Finall, you can export that graph as a simple text file of Edges +# for use in other programs. +# +# The class has multiple parameters that you can set to fine tune the nature of the graphs produced. For now, these are +# just set by editing the values in the __init__ function below. For instance, +# you can specify edge distance distortion, percentage of edges to drop (to make sparse graphs), min. distance between +# nodes generated, etc. See comments in the init function below +# +## Usage examples: +## x=GraphMaker(20) # create a new random 20 node map. +# +# x.plot() # now graphically show it +# x.edit() # enter edit mode. It repeatedly asks you to add/delete edges. +# x.export('outfile') # exports the current map to the file "outfile.txt" in the working dir + +# TO-DO: implement a load-graph-from-file to *reload* a graph after saving...in case you want to edit and re-save + +__author__ = "Eck Doerry" +__copyright__ = "Copyright 2018, Northern Arizona University, Flagstaff AZ" + + +import numpy as np +import scipy.spatial as graph +import matplotlib.pyplot as plt +from node import Node +from edge import Edge + +class GraphMaker: + + def __init__(self,nodeCount=10): + self.numNodes=nodeCount # number of nodes to create in graph. + self.expansion=2 # how wide the graph will be. Basically a factor that increases the x-y extents of plane + self.mindist = 0.3 * self.expansion # the minimum separation distance between nodes. Prevents cluttered graphs + # distDistortion. Often you might want to have a graph where the distances are *based on* the actual + # distances, but not exactly that. distDistortion is a percentage within which the cartesian distances + # between points will be randomly distorted while labeling the new edges in the graph. So at 0.3, 30% of the + # distance will be randomly distorted between 0 and 100%. So you could lose/gain 30% distance, randomly. + self.distDistortion = 0.5 # usually 0.3 is nice + # edgeLoss: in a roadmap, we don't want edges between EVERY point to EVERY other point. Hence we randomly drop some edges + # the edgeLoss specifies the percentage of edges we should randomly drop + self.edgeLoss= 0.1 + self.nodes=[] # array of node objects in this graph + self.edges=[] # array of edges in this graph + self.buildGraph() # Build the new graph! Commented out. Call it separately after creation! + + # if you don't like the graph generated, just try another one! + def redo(self,numNodes=-1): + self.edges=[] + self.nodes=[] + if numNodes>0: self.numNodes=numNodes + plt.close() + self.buildGraph() + self.plot() + + # Lets you interactively edit a graph to whack out edges + def edit(self): + entry="" + print("Edit Mode: {k/b}a,b to kill/build an edge between a,b ; r to replot, q to quit") + while True: + plt.pause(0.05) + entry=input("What now?") + entry = entry.strip() + if entry=='r': + self.replot() + elif entry=='q': break + else: + command=entry[0] + [start,end]=entry[1:].split(',') + if command=='b': self.buildEdge(start,end) + else: self.whackEdge(start,end) + print("End Edit mode") + + + # This method lets one whack out edges. So you can edit away edges after you generate the graph! + def whackEdge(self,node1,node2): + node1=node1.upper() + node2=node2.upper() + newEdges=[] + for edge in self.edges: + if edge.connects(node1,node2): # whack the edge + plt.plot([edge.x1, edge.x2], [edge.y1, edge.y2], 'b-', color='w', linewidth=0.5) # make the lines + plt.text(edge.midx, edge.midy, edge.label, size='x-small', color='w') # label the edge at its midpoint + plt.pause(0.05) + else: newEdges.append(edge) + self.edges=newEdges + + def buildEdge(self,node1,node2): + node1 = node1.upper() + node2 = node2.upper() + for edge in self.edges: # avoid building redundant new edges! + if edge.connects(node1,node2): + print("already an edge from "+node1+" to "+node2) + return + p1=self.nodeLocation(node1) + p2=self.nodeLocation(node2) + if p1!=0 and p2!=0: # both nodes exist + print("building edge!") + edge=Edge(p1,p2, self.distDistortion) # make a new edge + edge.setLabels(node1,node2) + self.edges.append(edge) + plt.plot([edge.x1, edge.x2], [edge.y1, edge.y2], 'b-', color='k', linewidth=0.5) # make the lines + plt.text(edge.midx, edge.midy, edge.label, size='x-small',color='k') # label the edge at its midpoint + else: print("start or end node doesn't exist") + + + # Just kill and redraw the plot. Useful after you've edited the graph + def replot(self): + plt.close() + self.plot() + plt.show() + + + # MAIN WORKER: Does the actual work of building a Roadmap. + # The biggest challenge here is linking the given points with a *non-intersecting* set of edges. In other words, + # we would like a "planar graph" imposed on these points. This is what Delauney function gives us: it basically + # finds non-overlapping triangles that cover all points; any one point is often a vertex in multiple triagles of + # the "puzzle" of triangles produced. We then need to extract unique edges and the nodes from this. + def buildGraph(self): + pointsArray = self.genPoints() # makes the array of (x,y) points to connect + tri=graph.Delaunay(pointsArray) # creates a planar graph on the given points. Creates a tri data structure. + triangles=pointsArray[tri.simplices] # list of triplets of three points. Each set of three points marks a + # triangle laid out by Delauney. Next need to extract the edges (point-to-point connections) from this. + # Now extract the unique edges from all the triangles to generate all edges in the graph + self.makeEdges(triangles) # populates self.edges with Edge objects + # Finally, turn the points array into actual Node objects. + Node.reset(Node) # Starts Node labeling at 'A' + self.nodes=list(map(Node,pointsArray[:,0], pointsArray[:,1])) #populates self.nodes with Node objects + self.labelEdges() # Just to make edge objects complete, go back and add endlabel (nodelabels) to those objects + + + # Makes all the edges in the new graph, based on the array of tris (basically triagles) produced by Delauney and + # pass in. Appends the new edges to the "edges" array. + def makeEdges(self,triArray): + edges = [] # temp array holding unique ((x,y),(x1,y1)) edge segments found so far + + # an important helper. When we extract a new edges from a triagle, it might well be the duplicate + # of an edge from an adjacent triangle created by Delauney. + def checkAddAdj(adjs): + for adj in adjs: + if (adj in edges) or ((adj[1], adj[0]) in edges): # if edges already in there, skip it + continue + else: + edges.append((adj[0], adj[1])) + # go through the triangle array, extract each edge, check that we don't have it already, and add to edges. + for triplet in triArray: + s = list(map(list,triplet)) + adjs = [(s[0], s[1]), (s[1], s[2]), (s[2], s[0])] # extracting the arcs between the points + checkAddAdj(adjs) + + # okay, now edges array contains all unique edge segment. Now make edge objects out of them! + # use edgeLoss value to randomly drop some pct of Edges + for edge in edges: + if (np.random.rand() > self.edgeLoss): + self.edges.append(Edge(edge[0],edge[1],self.distDistortion)) #make Edge object + else: print("blip") + + # For each edge, finds the label for its endpoints in the nodes, and adds to edge object. + def labelEdges(self): + for edge in self.edges: + edge.endlabel1=self.getNodeAtXY(edge.p1).label + edge.endlabel2=self.getNodeAtXY(edge.p2).label + + # Function to ask the roadgraph to plot itself. If not interactive, need to call plot.show() to see it. + def plot(self): + plt.ion # turn on interactive mode. Not sure if it's critical + # first plot the nodes + plt.plot([x.x for x in self.nodes], [x.y for x in self.nodes], 'ko', color="#BBBCBD") # plot vertices circles first + for node in self.nodes: # now plot the labels + plt.text(node.x, node.y, node.label, color='b', size='large', weight='normal') + # Now plot in the edges + for edge in self.edges: + plt.plot([edge.x1,edge.x2],[edge.y1,edge.y2], 'b-', color='k', linewidth=0.5) # make the lines + plt.text(edge.midx,edge.midy,edge.label, size='x-small') # label the edge at its midpoint + + + + # Function to return the roadgraph as simple list that can be fed to search program. + # prints it out, one line per edge. Format (nodeLabel1, nodeLabel2, edgeLabel, ((x1,y1),(x2,y2),(midx,midy)) ) + # if you pass it a text string, it will print to that, else prints to console + def export(self,outfile=0): + if outfile != 0: + outfilename=outfile+".txt" + out=open(outfilename,'w') + for edge in self.edges: + theEdge=((self.getNodeAtXY(edge.p1).label,self.getNodeAtXY(edge.p2).label,edge.label)) + locInfo=((edge.p1,edge.p2)) + if outfile: + out.write(str(theEdge+locInfo)+"\n") + else: + print(theEdge+locInfo) + if outfile: out.close() + print("tada!") + + + # Returns a node that is located at a given point + def getNodeAtXY(self,aPoint): # gets the Node at location x-y of point + for node in self.nodes: + if (node.x==aPoint[0]) and (node.y==aPoint[1]): return node + return 0 + + # Just returns x-y location of a node, given its label + def nodeLocation(self,label): # finds and returns the location of node labeled 'label' + for node in self.nodes: + if node.label==label: return [node.x,node.y] + print("nodeLoc: No node with label "+label+" exists") + + # a key function. Generates a random set of points that are some minimum distance apart. + def genPoints(self): + # While you still need points, generate random points, add to set if min dist from all currently found points. + points = [] # An array to put your points in as you find them. + found = 0 # Tracks points found so far + + # Simple function that checks that candidate points are some min dist from all other points. + # keeps it from making cluttered graphs! + def checkpt(apoint, allpoints): + for point in allpoints: + dst = graph.distance.euclidean(point, apoint) + if (dst < self.mindist): return False + return True + + while found < self.numNodes: + p1 = self.expansion * np.random.randn(2) + if checkpt(p1, points): + points.append(p1) + found = found + 1 + # print("found one:"+str(found)) + if len(points) == 0: points.append(p1) + + # Now just turn the points into a numpy array + pArray = np.array(points) + # print(points) + + # AWESOME. Now shift all the points into the positive x-y quadrant + xmin = min(pArray[:, [0]])[0] + ymin = min(pArray[:, [1]])[0] + xshift = abs(xmin) + 1 + yshift = abs(ymin) + 1 + posArray = pArray + [xshift, yshift] + posArray = posArray.astype(int) # truncate the entire array of points into integer values. Easier! + return posArray + + + #mainly for debugging: Functions to have the graph display its edges and nodes + def show(self): + print("Here is the graph:") + print("Nodes: ") + print([x.get() for x in self.nodes]) + print("Edges: ") + print([x.get() for x in self.edges]) + + def die(self): # just kills any open graph + plt.close() + +### UNIT TESTING +if __name__ == "__main__": + import sys + x=GraphMaker(sys.argv[1]) + x.export(sys.argv[2]) diff --git a/RouteFinding/node.py b/RouteFinding/node.py new file mode 100644 index 0000000..9e2a97e --- /dev/null +++ b/RouteFinding/node.py @@ -0,0 +1,66 @@ + +'''Node.py: This class implements simple node object and associated methods. + Nodes are created by passing in an x,y cartesian location for the node. Node labels are created + by just assigning letters to nodes in ascending ASCII order; current index is kept as class variable + to ensure that each new node has a unique label. + The most interesting internal method here is makeLabel, which generates unique labels. We want easy to read + alpha labels...so it basically counts up in Base26, generating the corresponding ASCII labels. Tricky! + ''' + +__author__ = "Eck Doerry" +__copyright__ = "Copyright 2018, Northern Arizona University, Flagstaff AZ" + +import scipy.spatial as graph +import numpy as np + +class Node: + +# CLASS VAR: curIndex is used to make sure that all nodes have a unique label. Incremented every time a new +# node is generated. + curIndex=1 + + # CLASS METHOD. Allows for reseting the index counter for generating new nodes. Used only for debugging! + def reset(cls): + cls.curIndex=1 + + # Create a new node at point x-y. Optionally pass in a label for the node. If no explicit label is passed + # in (which is usually the case), it auto-generates the next label in a sequences starting with A-Z, then + # going to AA, AB, AC, etc. + def __init__(self,newX,newY, newLabel= '*'): + self.x=newX + self.y=newY + if newLabel=='*': + self.label=self.makeLabel() + else: + self.label=newLabel + + # Makes an upper case label starting at 'A'. If >26 nodes, goes to 'AB', 'AC', etc. + # Works by basically counting in Base26: converts the index into strings in Base26! + def makeLabel(self): + index=self.curIndex + out = [] + while index>25: + out.append(index % 26) + index = int(index / 26) + + out.append(index-1) + out.reverse() + Node.curIndex +=1 + return "".join([chr(65+x) for x in out]) + + + # Distance: returns the cartesian distance between this node some other node that is passed in. + def distance(self,aNode): + return graph.distance.euclidean([self.x,self.y],[aNode.x,aNode.y]) + + def get(self): + return ([self.x,self.y],self.label) + + + +# Unit tests +# x=Node(4,5) +# y=Node(1,2) +# print(x.get()) +# print(y.get()) + diff --git a/RouteFinding/samplemap-1.txt b/RouteFinding/samplemap-1.txt new file mode 100644 index 0000000..728bddf --- /dev/null +++ b/RouteFinding/samplemap-1.txt @@ -0,0 +1,128 @@ +('AS', 'J', 99, [208, 215], [204, 116]) +('J', 'T', 25, [204, 116], [227, 105]) +('T', 'AS', 111, [227, 105], [208, 215]) +('M', 'AS', 49, [159, 212], [208, 215]) +('AS', 'AD', 79, [208, 215], [138, 252]) +('AD', 'M', 45, [138, 252], [159, 212]) +('AY', 'AI', 24, [33, 202], [56, 195]) +('AI', 'AD', 99, [56, 195], [138, 252]) +('AD', 'AY', 116, [138, 252], [33, 202]) +('O', 'J', 34, [181, 142], [204, 116]) +('AS', 'O', 77, [208, 215], [181, 142]) +('AJ', 'AV', 18, [172, 57], [176, 75]) +('AV', 'K', 32, [176, 75], [145, 84]) +('K', 'AJ', 38, [145, 84], [172, 57]) +('AA', 'AJ', 27, [145, 54], [172, 57]) +('K', 'AA', 30, [145, 84], [145, 54]) +('AK', 'M', 60, [110, 176], [159, 212]) +('AI', 'AK', 57, [56, 195], [110, 176]) +('M', 'AN', 47, [159, 212], [163, 165]) +('AN', 'AS', 67, [163, 165], [208, 215]) +('AN', 'O', 29, [163, 165], [181, 142]) +('AL', 'N', 53, [77, 80], [67, 27]) +('N', 'S', 17, [67, 27], [84, 31]) +('S', 'AL', 49, [84, 31], [77, 80]) +('AW', 'N', 47, [107, 1], [67, 27]) +('N', 'AX', 66, [67, 27], [1, 19]) +('AX', 'AW', 107, [1, 19], [107, 1]) +('AW', 'S', 37, [107, 1], [84, 31]) +('G', 'X', 28, [199, 81], [226, 72]) +('AW', 'D', 26, [107, 1], [132, 9]) +('D', 'S', 52, [132, 9], [84, 31]) +('D', 'AA', 46, [132, 9], [145, 54]) +('AA', 'S', 65, [145, 54], [84, 31]) +('AW', 'H', 65, [107, 1], [172, 1]) +('H', 'D', 40, [172, 1], [132, 9]) +('Y', 'C', 26, [98, 75], [94, 101]) +('C', 'AL', 27, [94, 101], [77, 80]) +('AL', 'Y', 21, [77, 80], [98, 75]) +('S', 'Y', 46, [84, 31], [98, 75]) +('AA', 'Y', 51, [145, 54], [98, 75]) +('A', 'B', 24, [92, 145], [95, 121]) +('C', 'I', 32, [94, 101], [108, 130]) +('B', 'C', 20, [95, 121], [94, 101]) +('AK', 'U', 16, [110, 176], [122, 165]) +('U', 'M', 59, [122, 165], [159, 212]) +('AN', 'U', 41, [163, 165], [122, 165]) +('U', 'AQ', 19, [122, 165], [140, 157]) +('AQ', 'AN', 24, [140, 157], [163, 165]) +('AU', 'AK', 26, [89, 160], [110, 176]) +('AI', 'AU', 48, [56, 195], [89, 160]) +('AV', 'AF', 19, [176, 75], [163, 90]) +('AF', 'K', 18, [163, 90], [145, 84]) +('AT', 'AP', 35, [30, 84], [58, 105]) +('AP', 'AR', 51, [58, 105], [7, 103]) +('AR', 'AT', 29, [7, 103], [30, 84]) +('AT', 'AL', 47, [30, 84], [77, 80]) +('AL', 'AP', 31, [77, 80], [58, 105]) +('W', 'C', 19, [76, 108], [94, 101]) +('B', 'W', 23, [95, 121], [76, 108]) +('W', 'AP', 18, [76, 108], [58, 105]) +('AL', 'W', 28, [77, 80], [76, 108]) +('X', 'R', 36, [226, 72], [196, 52]) +('R', 'AH', 20, [196, 52], [208, 36]) +('AH', 'X', 40, [208, 36], [226, 72]) +('G', 'R', 29, [199, 81], [196, 52]) +('R', 'H', 56, [196, 52], [172, 1]) +('H', 'AH', 50, [172, 1], [208, 36]) +('R', 'AJ', 24, [196, 52], [172, 57]) +('AJ', 'H', 56, [172, 57], [172, 1]) +('R', 'AV', 30, [196, 52], [176, 75]) +('G', 'AV', 23, [199, 81], [176, 75]) +('AG', 'D', 22, [150, 23], [132, 9]) +('H', 'AG', 31, [172, 1], [150, 23]) +('AG', 'AA', 31, [150, 23], [145, 54]) +('AJ', 'AG', 40, [172, 57], [150, 23]) +('E', 'AA', 37, [123, 84], [145, 54]) +('K', 'E', 22, [145, 84], [123, 84]) +('E', 'Y', 26, [123, 84], [98, 75]) +('E', 'C', 33, [123, 84], [94, 101]) +('AB', 'AN', 36, [137, 140], [163, 165]) +('U', 'AB', 29, [122, 165], [137, 140]) +('AB', 'O', 44, [137, 140], [181, 142]) +('AB', 'P', 29, [137, 140], [159, 121]) +('P', 'O', 30, [159, 121], [181, 142]) +('I', 'AE', 16, [108, 130], [108, 146]) +('AE', 'A', 16, [108, 146], [92, 145]) +('AE', 'AU', 23, [108, 146], [89, 160]) +('AU', 'A', 15, [89, 160], [92, 145]) +('AE', 'AB', 29, [108, 146], [137, 140]) +('U', 'AE', 23, [122, 165], [108, 146]) +('AK', 'AE', 30, [110, 176], [108, 146]) +('AU', 'AC', 38, [89, 160], [55, 142]) +('AC', 'A', 37, [55, 142], [92, 145]) +('W', 'AC', 39, [76, 108], [55, 142]) +('AC', 'AP', 37, [55, 142], [58, 105]) +('AI', 'AC', 53, [56, 195], [55, 142]) +('AC', 'B', 45, [55, 142], [95, 121]) +('AC', 'AR', 61, [55, 142], [7, 103]) +('AC', 'AY', 63, [55, 142], [33, 202]) +('AY', 'AR', 102, [33, 202], [7, 103]) +('O', 'AO', 36, [181, 142], [169, 108]) +('AO', 'J', 35, [169, 108], [204, 116]) +('P', 'AO', 16, [159, 121], [169, 108]) +('F', 'N', 18, [49, 32], [67, 27]) +('AL', 'F', 55, [77, 80], [49, 32]) +('AT', 'F', 55, [30, 84], [49, 32]) +('AM', 'AT', 27, [4, 76], [30, 84]) +('AR', 'AM', 27, [7, 103], [4, 76]) +('F', 'AM', 62, [49, 32], [4, 76]) +('AM', 'AX', 57, [4, 76], [1, 19]) +('Q', 'I', 19, [127, 124], [108, 130]) +('C', 'Q', 40, [94, 101], [127, 124]) +('E', 'Q', 40, [123, 84], [127, 124]) +('Q', 'AE', 29, [127, 124], [108, 146]) +('Q', 'AB', 18, [127, 124], [137, 140]) +('Q', 'P', 32, [127, 124], [159, 121]) +('AO', 'V', 25, [169, 108], [191, 96]) +('V', 'J', 23, [191, 96], [204, 116]) +('V', 'T', 37, [191, 96], [227, 105]) +('V', 'G', 17, [191, 96], [199, 81]) +('V', 'AV', 25, [191, 96], [176, 75]) +('AO', 'AF', 18, [169, 108], [163, 90]) +('L', 'E', 30, [150, 99], [123, 84]) +('K', 'L', 15, [145, 84], [150, 99]) +('L', 'Q', 33, [150, 99], [127, 124]) +('L', 'P', 23, [150, 99], [159, 121]) +('AF', 'L', 15, [163, 90], [150, 99]) +('L', 'AO', 21, [150, 99], [169, 108])