diff --git a/aequilibrae/paths/graph.py b/aequilibrae/paths/graph.py index 407c6fcb0..481fb04e4 100644 --- a/aequilibrae/paths/graph.py +++ b/aequilibrae/paths/graph.py @@ -3,7 +3,7 @@ from abc import ABC from datetime import datetime from os.path import join -from typing import List, Tuple, Optional +from typing import List, Tuple, Optional, Union import dataclasses import numpy as np @@ -274,6 +274,54 @@ def _build_directed_graph(self, network: pd.DataFrame, centroids: np.ndarray): return all_nodes, num_nodes, nodes_to_indices, fs, df + def compute_path( + self, + origin: int, + destination: int, + early_exit: bool = False, + a_star: bool = False, + heuristic: Union[str, None] = None, + ): + """ + Returns the results from path computation result holder. + + :Arguments: + **origin** (:obj:`int`): origin for the path + + **destination** (:obj:`int`): destination for the path + + **early_exit** (:obj:`bool`): stop constructing the shortest path tree once the destination is found. + Doing so may cause subsequent calls to ``update_trace`` to recompute the tree. Default is ``False``. + + **a_star** (:obj:`bool`): whether or not to use A* over Dijkstra's algorithm. + When ``True``, ``early_exit`` is always ``True``. Default is ``False``. + + **heuristic** (:obj:`str`): heuristic to use if ``a_star`` is enabled. Default is ``None``. + """ + from aequilibrae.paths import PathResults + + res = PathResults() + res.prepare(self) + res.compute_path(origin, destination, early_exit, a_star, heuristic) + + return res + + def compute_skims(self, cores: Union[int, None] = None): + """ + Returns the results from network skimming result holder. + + :Arguments: + **cores** (:obj:`Union[int, None]`): number of cores (threads) to be used in computation + """ + from aequilibrae.paths import NetworkSkimming + + skimmer = NetworkSkimming(self) + if cores: + skimmer.set_cores(cores) + skimmer.execute() + + return skimmer.results + def exclude_links(self, links: list) -> None: """ Excludes a list of links from a graph by setting their B node equal to their A node diff --git a/aequilibrae/paths/results/path_results.py b/aequilibrae/paths/results/path_results.py index 01d4455fe..7394548de 100644 --- a/aequilibrae/paths/results/path_results.py +++ b/aequilibrae/paths/results/path_results.py @@ -190,7 +190,7 @@ def set_heuristic(self, heuristic: str) -> None: **heuristic** (:obj:`str`): Heuristic to use in A*. """ if heuristic not in HEURISTIC_MAP.keys(): - raise ValueError(f"heruistic must be one of {self.get_heuristics()}") + raise ValueError(f"heuristic must be one of {self.get_heuristics()}") self._heuristic = heuristic diff --git a/docs/source/graph_example.py b/docs/source/graph_example.py new file mode 100644 index 000000000..1be6a081b --- /dev/null +++ b/docs/source/graph_example.py @@ -0,0 +1,106 @@ +""" +.. _plot_graph_from_arbitrary_data: + +Graph from arbitrary data +========================= + +In this example, we demonstrate how to create an AequilibraE Graph from an arbitrary network. + +We are using +`Sioux Falls data `_, +from TNTP. +""" + +# %% +# .. seealso:: +# Several functions, methods, classes and modules are used in this example: +# +# * :func:`aequilibrae.paths.Graph` + +# %% + +# Imports +import numpy as np +import pandas as pd + +from aequilibrae.paths import Graph +# sphinx_gallery_thumbnail_path = '../source/_images/graph_network.png' + +# %% +# We start by adding the path to load our arbitrary network. +net_file = "https://raw.githubusercontent.com/bstabler/TransportationNetworks/master/SiouxFalls/SiouxFalls_net.tntp" + +# %% +# Let's read our data! We'll be using Sioux Falls transportation network data, but without +# geometric information. The data will be stored in a Pandas DataFrame containing information +# about initial and final nodes, link distances, travel times, etc. +net = pd.read_csv(net_file, skiprows=8, sep="\t", lineterminator="\n", usecols=np.arange(1, 11)) + +# %% +# The Graph object requires several default fields: link_id, a_node, b_node, and direction. +# We'll need to manipulate the data to add the missing fields (link_id and direction) and +# rename the node columns accordingly. +net.insert(0, "link_id", np.arange(1, net.shape[0] + 1)) +net = net.assign(direction=1) +net.rename(columns={"init_node": "a_node", "term_node": "b_node"}, inplace=True) + +# %% +# Now we can take a look in our network file +net.head() + +# %% +# Building an AequilibraE graph from our network is pretty straightforward. We assign +# our network to be the graph's network ... +graph = Graph() +graph.network = net + +# %% +# ... and then set the graph's configurations. + +graph.prepare_graph(np.arange(1, 25)) # sets the centroids for which we will perform computation + +graph.set_graph("length") # sets the cost field for path computation + +graph.set_skimming(["length", "free_flow_time"]) # sets the skims to be computed + +graph.set_blocked_centroid_flows(False) # we don't block flows through centroids because all nodes + # in the Sioux Falls network are centroids + +# %% +# Two of AequilibraE's new features consist in directly computing path or skims. +# +# Let's compute the path between nodes 1 and 17... +res = graph.compute_path(1, 17) + +# %% +# ... and print the corresponding nodes... +res.path_nodes + +# %% +# ... and the path links. +res.path +# %% +# For path computation, when we call the method `graph.compute_path(1, 17)`, we are calling the class +# `PathComputation ` and storing its results into a variable. +# +# Notice that other methods related to path computation, such as `milepost` can also be used with +# `res`. + +# %% +# For skim computation, the process is quite similar. When calligng the method `graph.compute_skims()` +# we are actually calling the class `NetworkSkimming `, and storing +# its results into `skm`. + +skm = graph.compute_skims() + +# %% +# Let's get the values for 'free_flow_time' matrix. +skims = skm.skims +skims.get_matrix("free_flow_time") + +# %% +# Now we're all set! + +# %% +# Graph image credits to +# `Behance-network icons created by Sumitsaengtong - Flaticon `_ \ No newline at end of file diff --git a/tests/aequilibrae/paths/test_graph.py b/tests/aequilibrae/paths/test_graph.py index fd6e66c2a..9ac27d482 100644 --- a/tests/aequilibrae/paths/test_graph.py +++ b/tests/aequilibrae/paths/test_graph.py @@ -83,6 +83,28 @@ def test_available_skims(self): if i not in avail: self.fail("Skim availability with problems") + def test_compute_path(self): + graph = self.project.network.graphs["c"] + graph.prepare_graph() + graph.set_graph("distance") + graph.set_blocked_centroid_flows(False) + + res = graph.compute_path(1, 6) + self.assertEqual(list(res.path), [1, 4], "Number of path links is not correct") + self.assertEqual(list(res.path_nodes), [1, 2, 6], "Number of path nodes is not correct") + + def test_compute_skims(self): + graph = self.project.network.graphs["c"] + graph.prepare_graph() + graph.set_graph("distance") + graph.set_skimming(["distance", "free_flow_time"]) + graph.set_blocked_centroid_flows(False) + + skm = graph.compute_skims() + skims = skm.skims + self.assertEqual(skims.cores, 2, "Number of cores is not correct") + self.assertEqual(skims.names, ["distance", "free_flow_time"], "Matrices names are not correct") + def test_exclude_links(self): # excludes a link before any setting or preparation self.graph.set_blocked_centroid_flows(False)