Example #1
0
    def __init__(self, file_name):
        """ Initialize with a graph structure which will be evaluated

            :param file_name: a text file with the routes and its weights
        """
        self._graph = Graph(file_name).graph()
Example #2
0
class GraphMetrics:

    def __init__(self, file_name):
        """ Initialize with a graph structure which will be evaluated

            :param file_name: a text file with the routes and its weights
        """
        self._graph = Graph(file_name).graph()

    def _get_route(self, route):
        """ Gets the route vertices in the "A-D-C-E" format
            and splits it into a list of route vertices.

            :param route: String with a sequence of vertices separated
                          by a '-' character

            :returns: a list of vertices
        """
        return route.split("-")

    def path_distance(self, route):
        """ Evaluates the distance between vertices passing
            by specific vertices.

            :param route: String with a sequence of vertices separated
                          by a '-' character

            :returns: a int representing the total distance between
                      vertices. If the vertice is unreachable a
                      "NO SUCH ROUTE" message is presented
        """
        nodes = self._get_route(route)
        total_distance = 0
        for i, node in enumerate(nodes[:-1]):
            distance = self._graph.get(nodes[i]).get(nodes[i+1])
            if distance is None:
                return "NO SUCH ROUTE"
            total_distance += distance
        return total_distance

    # The number of trips starting at C and
    # ending at C with a maximum of 3 stops
    def trips_with_max_stops(self, start, objective, max_stops, n_stops_cycle, stops=0):
        """ Evaluates stops between vertices limited by a maximum value

            :param start: A string vertice where the trip starts
            :param objective: A string vertice where the trip ends
            :param max_stops: A int value representing the upper bound of stops
                              made between start and objective
            :param n_stops_cycle: A list with all numbers of stops
                                  between start and end.
            :param stops: a counter to control how many stops has been made so far

            :returns: a sum of all stops that is less than or equal to max_stops
        """
        # counts the stops so far
        stops += 1

        # for each node adjacent to start, check if it's the objective
        # if it is, adds to a list the value of stops made until then
        # else, accesses recursively the adjacent nodes.
        # In the end, returns the sum of all stops that are more
        # than or equal to max_stops
        for node in self._graph[start]:

            if node == objective:
                n_stops_cycle.append(stops)
            else:
                self.trips_with_max_stops(node, objective, max_stops, n_stops_cycle, stops)
        return sum([1 for x in n_stops_cycle if x <= max_stops])

    # Has one problem here!
    # If the keys are not sorted the behavior is inconstant.
    # The answer depends on the order which the neighbor is visited.
    # For example:
    #   when the node D is visited the two possibles neighbors are C and E
    #   if we first visit C it marks C as visited then the recursion goes to
    #   the next D neighbor (E). When it gets there,
    #   C somehow has been marked as visited
    #   then the recursion stops when the next C is found.
    #   This doesn't happen when the order is the opposite.
    def trips_with_exact_stops(self, start, objective, max_stops, n_stops_cycle, stops=0, visited=False):
        """ Evaluates stops between vertices limited with an exact value

            :param start: A string vertice where the trip starts
            :param objective: A string vertice where the trip ends
            :param max_stops: A int value representing the exact value of stops
                              made between start and objective that must be achieved
            :param n_stops_cycle: A list with all numbers of stops 
                                  between start and end.
            :param stops: a counter to control how many stops has been made so far
            :param visited: a boolean flag to control if the end 
                            vertice has already been visited

            :returns: a sum of all stops that is equal to max_stops
        """
        # counts the stops made so far
        stops += 1

        # for each node adjacent to start, check if it's the objective
        # and if has already been visited
        # if it is, adds to a list the value of stops made until then
        # else, checks if current is equals to objective
        # if it is, mark it as visited
        # then accesses recursively the adjacent nodes.
        # In the end, returns the sum of all stops that are exactly
        # equal to max_stops
        for node in sorted(self._graph[start]):
            if node == objective and visited is True:
                n_stops_cycle.append(stops)
            else:
                if node == objective:
                    visited = True
                self.trips_with_exact_stops(node, objective, max_stops, n_stops_cycle, stops, visited)
        return sum([1 for x in n_stops_cycle if x == (max_stops + 1)])

    def dijkstra(self, start, objective):
        """ Implementation of Dijkstra Algorithm to find
            the shortest path between vertices
            Reference: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm

            :param start: A string with initial vertice
            :param objective: A string with the objective vertice

            :returns: a int representing the minimum distance value
                      between vertices start and objective
        """
        # sets a variable as infinity
        inf = float('inf')
        # initialize the distances with infinity values for each node
        distances = {key: inf for key in self._graph.keys()}
        # each parent is set as None
        parent = {key: None for key in self._graph.keys()}
        # nodes are set to infinity. This structure is used as a queue
        # to control the minimum distance
        nodes = {key: inf for key in self._graph.keys()}

        # then initializes the start node with 0
        distances[start] = 0
        nodes[start] = 0

        # while node is not empty
        while nodes:
            # gets de smallest distance on node
            smallest, nodes = self._get_smallest(nodes)

            # checks if this node is unreachable
            if smallest is None or distances[smallest] == inf:
                break

            # The variable alt is the length of the path from the start node
            # to the neighbor node. If this path is shorter than the current
            # shortest path, that current path is replaced with this alt path.
            for neighbor in self._graph[smallest]:
                alt = distances[smallest] + self._graph[smallest][neighbor]
                if alt < distances[neighbor]:
                    distances[neighbor] = alt
                    parent[neighbor] = smallest
                    nodes[neighbor] = alt

        return distances.get(objective, 0)

    def _get_smallest(self, nodes):
        """ Gets the smallest value from a route dictionary
            representing the possible next step from
            the current vertice

            :param nodes: a dictionary representing a vertice neighbor
                          and its weight

            :returns: the smallest neighbor value
                      and remaining neighbors dictionary
        """
        # gets the minimum value from node dictionary
        # removes it, then returns the smallest and the node dictionary
        smallest = min(nodes, key=nodes.get)
        del nodes[smallest]
        return smallest, nodes

    # The length of the shortest route (in terms of distance to travel) from A to C
    def shortest_path(self, origin, objective):
        """ Gets the shortest path between two vertices

            :param origin: A string with the origin vertice
            :param objective: A string with the objective vertice

            :returns: the value with the minimum distance value
                      between origin and objective
        """
        # if the origin is equal to objective the Dijsktra algortithm returns
        # a value of 0 because it considers the distance to itself 0.
        # To overcome this, the problem was split in two:
        #   - from start to next shortest path neighbr
        #   - then from this smallest neighbor to the objective
        if origin == objective:
            shortest_paths = { x: self.dijkstra(x, objective) for x in self._graph[objective].keys()}
            subsequent_shortest_path = min(shortest_paths, key=shortest_paths.get)
            min_value = min(shortest_paths.values())
            shortest_path_from = self.dijkstra(origin, subsequent_shortest_path)
            return sum([min_value, shortest_path_from])
        return self.dijkstra(origin, objective)

    def num_routes(self, start, objective, distance):
        """ Gets the number of routes made between vertices start
            and objective with a maximum distance

            :param start: A string with the origin vertice
            :param objective: A string with the objective vertice

            :returns: a number of routes with maximum distance
        """
        # gets all routes from start node to all nodes with maximum distance
        all_routes = self._different_routes_from_node_max_distance(start, distance)
        num_routes = 0

        # checks if the route starts with the start node
        # and ends with the objective node
        for route in all_routes:
            if route.startswith(start) and route.endswith(objective):
                num_routes += 1
        return num_routes

    def _join_paths(self, paths):
        """ Concatenates a list of strings

            :param paths: a list of a list of strings

            :returns: a list with joined strings
        """
        return ["".join(x) for x in paths]

    # The number of different routes from C to C with a distance of less than 30
    def _different_routes_from_node_max_distance(self, start, max_distance, curr_dist=0, path=[]):
        """ Evaluates the number of routes between two vertices limited 
            by an upper bound value

            :param start: A string vertice where the trip starts
            :param objective: A string vertice where the trip ends
            :param max_distance: A int value representing the maximum distance
                                 between start and objective that must be achieved
            :param curr_dist: a counter to control the distance made so far
            :param path: a list of all paths with maximum distance (max_distance)

            :returns: a list of all paths with maximum distance (max_distance)
        """
        # all nodes visited so far
        path = path + [start]

        # if the distance so far is more than the maximum distance
        # then it removes this node from the path and returns all
        # this route
        if curr_dist > max_distance:
            path.pop()
            return [path]

        paths = []

        # for each node adjacent to start
        # gets the distance from the start to it
        # then calls the method recursively to check all paths
        for node in self._graph[start]:
            distance = self._graph.get(start).get(node)
            new_paths = self._different_routes_from_node_max_distance(node, max_distance, curr_dist + distance, path)

            # adds all paths found so far that is 
            for new_path in new_paths:
                paths.append(new_path)
        return self._join_paths(paths)