diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 6db583769..bad2dacf5 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -45,8 +45,14 @@ jobs: - name: Test docstrings run: | - python -m pytest --doctest-modules aequilibrae/log.py aequilibrae/parameters.py aequilibrae/paths/vdf.py - python -m pytest docs/source/modeling_with_aequilibrae/project_pieces --doctest-glob=*.rst + python -m pytest --doctest-modules aequilibrae/distribution --ignore=aequilibrae/distribution/setup_ipf.py + python -m pytest --doctest-modules aequilibrae/matrix + python -m pytest --doctest-modules aequilibrae/paths --ignore=aequilibrae/paths/setup_assignment.py + python -m pytest --doctest-modules aequilibrae/project + python -m pytest --doctest-modules aequilibrae/log.py aequilibrae/parameters.py + python -m pytest --doctest-modules aequilibrae/transit + python -m pytest --doctest-glob=*.rst docs/source/modeling_with_aequilibrae/project_pieces + python -m pytest --doctest-glob=*.rst docs/source/modeling_with_aequilibrae/static_traffic_assignment - name: Build documentation run: | diff --git a/.gitignore b/.gitignore index 2153bfdde..868fdbd44 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,4 @@ coverage.xml #### End snippet .DS_Store +.vscode diff --git a/aequilibrae/distribution/gravity_application.py b/aequilibrae/distribution/gravity_application.py index 1b36e5392..be5249f2d 100644 --- a/aequilibrae/distribution/gravity_application.py +++ b/aequilibrae/distribution/gravity_application.py @@ -19,35 +19,32 @@ class GravityApplication: """Applies a synthetic gravity model. Model is an instance of SyntheticGravityModel class. + Impedance is an instance of AequilibraEMatrix. + Row and Column vectors are instances of AequilibraeData. .. code-block:: python - >>> import pandas as pd - >>> from aequilibrae import Project - >>> from aequilibrae.matrix import AequilibraeMatrix, AequilibraeData + >>> from aequilibrae.matrix import AequilibraeData >>> from aequilibrae.distribution import SyntheticGravityModel, GravityApplication - >>> project = Project.from_path("/tmp/test_project_ga") + >>> project = create_example(project_path) # We define the model we will use >>> model = SyntheticGravityModel() # Before adding a parameter to the model, you need to define the model functional form - >>> model.function = "GAMMA" # "EXPO" or "POWER" + # You can select one of GAMMA, EXPO or POWER. + >>> model.function = "GAMMA" # Only the parameter(s) applicable to the chosen functional form will have any effect >>> model.alpha = 0.1 >>> model.beta = 0.0001 - # Or you can load the model from a file - # model.load('path/to/model/file') - # We load the impedance matrix - >>> matrix = AequilibraeMatrix() - >>> matrix.load('/tmp/test_project_ga/matrices/skims.omx') - >>> matrix.computational_view(['distance_blended']) + >>> matrix = project.matrices.get_matrix("skims") + >>> matrix.computational_view(["distance_blended"]) # We create the vectors we will use >>> query = "SELECT zone_id, population, employment FROM zones;" @@ -61,8 +58,11 @@ class GravityApplication: >>> zones = df.index.shape[0] # We create the vector database - >>> args = {"entries": zones, "field_names": ["productions", "attractions"], - ... "data_types": [np.float64, np.float64], "memory_mode": True} + >>> args = {"entries": zones, + ... "field_names": ["productions", "attractions"], + ... "data_types": [np.float64, np.float64], + ... "memory_mode": True} + >>> vectors = AequilibraeData() >>> vectors.create_empty(**args) @@ -81,20 +81,14 @@ class GravityApplication: ... "model": model, ... "columns": vectors, ... "column_field": "attractions", - ... "output": '/tmp/test_project_ga/matrices/matrix.aem', + ... "output": os.path.join(project_path, 'matrices/gravity_matrix.aem'), ... "nan_as_zero":True ... } >>> gravity = GravityApplication(**args) # Solve and save the outputs >>> gravity.apply() - >>> gravity.output.export('/tmp/test_project_ga/matrices/omx_file.omx') - - # To save your report into a file, you can do the following: - # with open('/tmp/test_project_ga/report.txt', 'w') as file: - # for line in gravity.report: - # file.write(f"{line}\\n") - + >>> gravity.output.export(os.path.join(project_path, 'matrices/gravity_omx.omx')) """ def __init__(self, project=None, **kwargs): diff --git a/aequilibrae/distribution/gravity_calibration.py b/aequilibrae/distribution/gravity_calibration.py index 4e9f3d152..2d96ec1d3 100644 --- a/aequilibrae/distribution/gravity_calibration.py +++ b/aequilibrae/distribution/gravity_calibration.py @@ -17,29 +17,24 @@ class GravityCalibration: """Calibrate a traditional gravity model - Available deterrence function forms are: 'EXPO' or 'POWER'. 'GAMMA' + Available deterrence function forms are: 'EXPO', 'POWER' or 'GAMMA'. .. code-block:: python - >>> from aequilibrae import Project - >>> from aequilibrae.matrix import AequilibraeMatrix >>> from aequilibrae.distribution import GravityCalibration - >>> project = Project.from_path("/tmp/test_project_gc") + >>> project = create_example(project_path) - # We load the impedance matrix - >>> matrix = AequilibraeMatrix() - >>> matrix.load('/tmp/test_project_gc/matrices/demand.omx') - >>> matrix.computational_view(['matrix']) + # We load the demand matrix + >>> demand = project.matrices.get_matrix("demand_omx") + >>> demand.computational_view() - # We load the impedance matrix - >>> impedmatrix = AequilibraeMatrix() - >>> impedmatrix.load('/tmp/test_project_gc/matrices/skims.omx') - >>> impedmatrix.computational_view(['time_final']) + # We load the skim matrix + >>> skim = project.matrices.get_matrix("skims") + >>> skim.computational_view(["time_final"]) - # Creates the problem - >>> args = {"matrix": matrix, - ... "impedance": impedmatrix, + >>> args = {"matrix": demand, + ... "impedance": skim, ... "row_field": "productions", ... "function": 'expo', ... "nan_as_zero": True} @@ -47,12 +42,7 @@ class GravityCalibration: # Solve and save outputs >>> gravity.calibrate() - >>> gravity.model.save('/tmp/test_project_gc/dist_expo_model.mod') - - # To save the model report in a file - # with open('/tmp/test_project_gc/report.txt', 'w') as f: - # for line in gravity.report: - # f.write(f'{line}\\n') + >>> gravity.model.save(os.path.join(project_path, 'dist_expo_model.mod')) """ def __init__(self, project=None, **kwargs): diff --git a/aequilibrae/distribution/ipf.py b/aequilibrae/distribution/ipf.py index 25290f292..a6bd52bac 100644 --- a/aequilibrae/distribution/ipf.py +++ b/aequilibrae/distribution/ipf.py @@ -17,23 +17,21 @@ class Ipf: .. code-block:: python - >>> from aequilibrae import Project >>> from aequilibrae.distribution import Ipf - >>> from aequilibrae.matrix import AequilibraeMatrix, AequilibraeData + >>> from aequilibrae.matrix import AequilibraeData - >>> project = Project.from_path("/tmp/test_project_ipf") + >>> project = create_example(project_path) - >>> matrix = AequilibraeMatrix() - - # Here we can create from OMX or load from an AequilibraE matrix. - >>> matrix.load('/tmp/test_project/matrices/demand.omx') + >>> matrix = project.matrices.get_matrix("demand_omx") >>> matrix.computational_view() - >>> args = {"entries": matrix.zones, "field_names": ["productions", "attractions"], - ... "data_types": [np.float64, np.float64], "memory_mode": True} + >>> dataset_args = {"entries": matrix.zones, + ... "field_names": ["productions", "attractions"], + ... "data_types": [np.float64, np.float64], + ... "memory_mode": True} >>> vectors = AequilibraeData() - >>> vectors.create_empty(**args) + >>> vectors.create_empty(**dataset_args) >>> vectors.productions[:] = matrix.rows()[:] >>> vectors.attractions[:] = matrix.columns()[:] @@ -41,17 +39,19 @@ class Ipf: # We assume that the indices would be sorted and that they would match the matrix indices >>> vectors.index[:] = matrix.index[:] - >>> args = { - ... "matrix": matrix, "rows": vectors, "row_field": "productions", "columns": vectors, - ... "column_field": "attractions", "nan_as_zero": False} - - >>> fratar = Ipf(**args) + >>> ipf_args = {"matrix": matrix, + ... "rows": vectors, + ... "row_field": "productions", + ... "columns": vectors, + ... "column_field": "attractions", + ... "nan_as_zero": False} + >>> fratar = Ipf(**ipf_args) >>> fratar.fit() # We can get back to our OMX matrix in the end - >>> fratar.output.export("/tmp/to_omx_output.omx") - >>> fratar.output.export("/tmp/to_aem_output.aem") + >>> fratar.output.export(os.path.join(my_folder_path, "to_omx_output.omx")) + >>> fratar.output.export(os.path.join(my_folder_path, "to_omx_output.aem")) """ def __init__(self, project=None, **kwargs): diff --git a/aequilibrae/log.py b/aequilibrae/log.py index b2cd085c9..22efd8922 100644 --- a/aequilibrae/log.py +++ b/aequilibrae/log.py @@ -10,10 +10,8 @@ class Log: .. code-block:: python - >>> from aequilibrae import Project - >>> project = Project() - >>> project.new(tmp_path_empty) + >>> project.new(project_path) >>> log = project.log() diff --git a/aequilibrae/matrix/aequilibrae_data.py b/aequilibrae/matrix/aequilibrae_data.py index 3c383ab89..fc5b54fb4 100644 --- a/aequilibrae/matrix/aequilibrae_data.py +++ b/aequilibrae/matrix/aequilibrae_data.py @@ -12,7 +12,30 @@ class AequilibraeData(object): - """AequilibraE dataset""" + """AequilibraE dataset + + .. code-block:: python + + >>> from aequilibrae.matrix import AequilibraeData + + >>> args = {"file_path": os.path.join(my_folder_path, "vectors.aed"), + ... "entries": 3, + ... "field_names": ["production"], + ... "data_types": [np.float64]} + + >>> vectors = AequilibraeData() + >>> vectors.create_empty(**args) + + >>> vectors.production[:] = [1, 4, 7] + >>> vectors.index[:] = [1, 2, 3] + + >>> vectors.export(os.path.join(my_folder_path, "vectors.csv")) + + >>> reload_dataset = AequilibraeData() + >>> reload_dataset.load(os.path.join(my_folder_path, "vectors.aed")) + >>> reload_dataset.production + memmap([1., 4., 7.]) + """ def __init__(self): self.data = None @@ -37,8 +60,8 @@ def create_empty( Creates a new empty dataset :Arguments: - **file_path** (:obj:`str`, *Optional*): Full path for the output data file. If *memory_mode* is 'false' and - path is missing, then the file is created in the temp folder + **file_path** (:obj:`str`, *Optional*): Full path for the output data file. If 'memory_mode' is ``False`` + and path is missing, then the file is created in the temp folder **entries** (:obj:`int`, *Optional*): Number of records in the dataset. Default is 1 @@ -51,27 +74,6 @@ def create_empty( **memory_mode** (:obj:`bool`, *Optional*): If ``True``, dataset will be kept in memory. If ``False``, the dataset will be a memory-mapped numpy array - - .. code-block:: python - - >>> from aequilibrae.matrix import AequilibraeData, AequilibraeMatrix - - >>> mat = AequilibraeMatrix() - >>> mat.load('/tmp/test_project/matrices/demand.omx') - >>> mat.computational_view() - - >>> vectors = "/tmp/test_project/vectors.aed" - - >>> args = { - ... "file_path": vectors, - ... "entries": mat.zones, - ... "field_names": ["origins", "destinations"], - ... "data_types": [np.float64, np.float64] - ... } - - >>> dataset = AequilibraeData() - >>> dataset.create_empty(**args) - """ if file_path is not None or memory_mode: @@ -145,13 +147,6 @@ def load(self, file_path): :Arguments: **file_path** (:obj:`str`): Full file path to the AequilibraeData to be loaded - - .. code-block:: python - - >>> from aequilibrae.matrix import AequilibraeData - - >>> dataset = AequilibraeData() - >>> dataset.load("/tmp/test_project/vectors.aed") """ f = open(file_path) self.file_path = os.path.realpath(f.name) @@ -173,14 +168,6 @@ def export(self, file_name, table_name="aequilibrae_table"): **file_name** (:obj:`str`): File name with PATH and extension (csv, or sqlite3, sqlite or db) **table_name** (:obj:`str`): It only applies if you are saving to an SQLite table. Otherwise ignored - - .. code-block:: python - - >>> from aequilibrae.matrix import AequilibraeData - - >>> dataset = AequilibraeData() - >>> dataset.load("/tmp/test_project/vectors.aed") - >>> dataset.export("/tmp/test_project/vectors.csv") """ file_type = os.path.splitext(file_name)[1] @@ -237,9 +224,8 @@ def random_name(): >>> from aequilibrae.matrix import AequilibraeData - >>> name = AequilibraeData().random_name() # doctest: +ELLIPSIS - - # This is an example of output - # '/tmp/Aequilibrae_data_5werr5f36-b123-asdf-4587-adfglkjhqwe.aed' + >>> dataset = AequilibraeData() + >>> dataset.random_name() # doctest: +ELLIPSIS + '/tmp/Aequilibrae_data_...' """ return os.path.join(tempfile.gettempdir(), f"Aequilibrae_data_{uuid.uuid4()}.aed") diff --git a/aequilibrae/matrix/aequilibrae_matrix.py b/aequilibrae/matrix/aequilibrae_matrix.py index 9b7740381..2a7d0249d 100644 --- a/aequilibrae/matrix/aequilibrae_matrix.py +++ b/aequilibrae/matrix/aequilibrae_matrix.py @@ -197,10 +197,10 @@ def create_empty( >>> names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] >>> mat = AequilibraeMatrix() - >>> mat.create_empty(file_name='/tmp/path_to_matrix.aem', + >>> mat.create_empty(file_name=os.path.join(my_folder_path, 'my_matrix.aem'), ... zones=zones_in_the_model, ... matrix_names=names_list, - ... memory_only=False,) + ... memory_only=False) >>> mat.num_indices 1 >>> mat.zones @@ -675,7 +675,7 @@ def set_index(self, index_to_set: str) -> None: >>> index_list = ['tazs', 'census'] >>> mat = AequilibraeMatrix() - >>> mat.create_empty(file_name="/tmp/path_to_new_matrix.aem", + >>> mat.create_empty(file_name=os.path.join(my_folder_path, 'my_matrix.aem'), ... zones=zones_in_the_model, ... matrix_names=names_list, ... index_names=index_list ) @@ -767,15 +767,14 @@ def export(self, output_name: str, cores: List[str] = None): >>> names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] >>> mat = AequilibraeMatrix() - >>> mat.create_empty(file_name='/tmp/path_to_matrix.aem', + >>> mat.create_empty(file_name=os.path.join(my_folder_path, 'my_matrix.aem'), ... zones=zones_in_the_model, ... matrix_names=names_list) - >>> mat.cores - 5 - >>> mat.export('/tmp/my_new_path.aem', ['Car trips', 'bike trips']) + + >>> mat.export(os.path.join(my_folder_path, 'my_new_path.aem'), ['Car trips', 'bike trips']) >>> mat2 = AequilibraeMatrix() - >>> mat2.load('/tmp/my_new_path.aem') + >>> mat2.load(os.path.join(my_folder_path, 'my_new_path.aem')) >>> mat2.cores 2 """ @@ -826,11 +825,13 @@ def load(self, file_path: str): >>> from aequilibrae.matrix import AequilibraeMatrix + >>> project = create_example(project_path) + >>> mat = AequilibraeMatrix() - >>> mat.load('/tmp/path_to_matrix.aem') - >>> mat.computational_view(["bike trips"]) + >>> mat.load(os.path.join(project_path, 'matrices/skims.omx')) + >>> mat.computational_view() >>> mat.names - ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] + ['distance_blended', 'time_final'] """ self.file_path = file_path @@ -865,7 +866,7 @@ def computational_view(self, core_list: List[str] = None): >>> names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] >>> mat = AequilibraeMatrix() - >>> mat.create_empty(file_name='/tmp/path_to_matrix.aem', + >>> mat.create_empty(file_name=os.path.join(my_folder_path, 'my_matrix.aem'), ... zones=zones_in_the_model, ... matrix_names=names_list) >>> mat.computational_view(['bike trips', 'walk trips']) @@ -941,15 +942,18 @@ def copy( >>> names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] >>> mat = AequilibraeMatrix() - >>> mat.create_empty(file_name='/tmp/path_to_matrix.aem', zones=zones_in_the_model, matrix_names= names_list) - >>> mat.copy('/tmp/path_to_copy.aem', + >>> mat.create_empty(file_name=os.path.join(my_folder_path, 'my_matrix.aem'), + ... zones=zones_in_the_model, + ... matrix_names=names_list) + + >>> mat.copy(os.path.join(my_folder_path, 'copy_of_my_matrix.aem'), ... cores=['bike trips', 'walk trips'], ... names=['bicycle', 'walking'], ... memory_only=False) # doctest: +ELLIPSIS >>> mat2 = AequilibraeMatrix() - >>> mat2.load('/tmp/path_to_copy.aem') + >>> mat2.load(os.path.join(my_folder_path, 'copy_of_my_matrix.aem')) >>> mat2.cores 2 """ @@ -1005,8 +1009,10 @@ def rows(self) -> np.ndarray: >>> from aequilibrae.matrix import AequilibraeMatrix + >>> project = create_example(project_path) + >>> mat = AequilibraeMatrix() - >>> mat.load('/tmp/test_project/matrices/skims.omx') + >>> mat.load(os.path.join(project_path, 'matrices/skims.omx')) >>> mat.computational_view(["distance_blended"]) >>> mat.rows() array([357.68202084, 358.68778868, 310.68285491, 275.87964738, @@ -1032,8 +1038,10 @@ def columns(self) -> np.ndarray: >>> from aequilibrae.matrix import AequilibraeMatrix + >>> project = create_example(project_path) + >>> mat = AequilibraeMatrix() - >>> mat.load('/tmp/test_project/matrices/skims.omx') + >>> mat.load(os.path.join(project_path, 'matrices/skims.omx')) >>> mat.computational_view(["distance_blended"]) >>> mat.columns() array([357.54256811, 357.45109051, 310.88655449, 276.6783439 , @@ -1053,10 +1061,23 @@ def nan_to_num(self): >>> from aequilibrae.matrix import AequilibraeMatrix + >>> nan_matrix = np.empty((3,3)) + >>> nan_matrix[:] = np.nan + + >>> index = np.arange(1, 4, dtype=np.int32) + >>> mat = AequilibraeMatrix() - >>> mat.load('/tmp/path_to_matrix.aem') - >>> mat.computational_view(["bike trips"]) + >>> mat.create_empty(file_name=os.path.join(my_folder_path, "matrices/nan_matrix.aem"), + ... zones=3, + ... matrix_names=["only_nan"]) + >>> mat.index[:] = index[:] + >>> mat.matrix["only_nan"][:, :] = nan_matrix[:, :] + >>> mat.computational_view() >>> mat.nan_to_num() + >>> mat.get_matrix("only_nan") + array([[0., 0., 0.], + [0., 0., 0.], + [0., 0., 0.]]) """ if self.__omx: @@ -1092,7 +1113,7 @@ def __define_data_class(self): def setName(self, matrix_name: str): """ - Sets the name for the matrix itself + Sets the name for the matrix itself. Only works for matrices in disk. :Arguments: **matrix_name** (:obj:`str`): matrix name. Maximum length is 50 characters @@ -1101,11 +1122,20 @@ def setName(self, matrix_name: str): >>> from aequilibrae.matrix import AequilibraeMatrix + >>> zones_in_the_model = 3317 + >>> mat = AequilibraeMatrix() - >>> mat.create_empty(file_name="matrix.aem", zones=3317, memory_only=False) + >>> mat.create_empty(file_name=os.path.join(my_folder_path, 'my_matrix.aem'), + ... zones=zones_in_the_model, + ... memory_only=False) >>> mat.setName('This is my example') - >>> mat.name - '' + >>> mat.save() + >>> mat.close() + + >>> mat = AequilibraeMatrix() + >>> mat.load(os.path.join(my_folder_path, 'my_matrix.aem')) + >>> mat.name.decode('utf-8') + 'This is my example' """ if self.__omx: raise NotImplementedError("This operation does not make sense for OMX matrices") @@ -1130,16 +1160,20 @@ def setDescription(self, matrix_description: str): >>> from aequilibrae.matrix import AequilibraeMatrix + >>> zones_in_the_model = 3317 + >>> mat = AequilibraeMatrix() - >>> mat.create_empty(file_name="matrix.aem", zones=3317, memory_only=False) - >>> mat.setDescription('This is some text about this matrix of mine') + >>> mat.create_empty(file_name=os.path.join(my_folder_path, 'my_matrix.aem'), + ... zones=zones_in_the_model, + ... memory_only=False) + >>> mat.setDescription('This is a text') >>> mat.save() >>> mat.close() >>> mat = AequilibraeMatrix() - >>> mat.load("matrix.aem") + >>> mat.load(os.path.join(my_folder_path, 'my_matrix.aem')) >>> mat.description.decode('utf-8') - 'This is some text ab' + 'This is a text' """ if self.__omx: raise NotImplementedError("This operation does not make sense for OMX matrices") @@ -1168,9 +1202,8 @@ def random_name() -> str: >>> from aequilibrae.matrix import AequilibraeMatrix - >>> name = AequilibraeMatrix().random_name() # doctest: +ELLIPSIS - - # This is an example of output - # '/tmp/Aequilibrae_matrix_54625f36-bf41-4c85-80fb-7fc2e3f3d76e.aem' + >>> mat = AequilibraeMatrix() + >>> mat.random_name() # doctest: +ELLIPSIS + '/tmp/Aequilibrae_matrix_...' """ return os.path.join(tempfile.gettempdir(), f"Aequilibrae_matrix_{uuid.uuid4()}.aem") diff --git a/aequilibrae/parameters.py b/aequilibrae/parameters.py index c23f3e07b..f0b33d963 100644 --- a/aequilibrae/parameters.py +++ b/aequilibrae/parameters.py @@ -27,14 +27,14 @@ class Parameters: .. code-block:: python - >>> from aequilibrae import Project, Parameters + >>> from aequilibrae import Parameters >>> project = Project() - >>> project.new(tmp_path_empty) + >>> project.new(project_path) >>> p = Parameters(project) - >>> p.parameters['system']['logging_directory'] = "/tmp/other_folder" + >>> p.parameters['system']['logging_directory'] = "/path_to/other_logging_directory" >>> p.parameters['osm']['overpass_endpoint'] = "http://192.168.0.110:32780/api" >>> p.parameters['osm']['max_query_area_size'] = 10000000000 >>> p.parameters['osm']['sleeptime'] = 0 diff --git a/aequilibrae/paths/assignment_paths.py b/aequilibrae/paths/assignment_paths.py index 0f397708c..8ddd582a8 100644 --- a/aequilibrae/paths/assignment_paths.py +++ b/aequilibrae/paths/assignment_paths.py @@ -57,13 +57,7 @@ def get_traffic_class_names_and_id(self) -> List[TrafficClassIdentifier]: class AssignmentPaths(object): - """Class for accessing path files optionally generated during assignment. - - .. code-block:: python - - paths = AssignmentPath(table_name_with_assignment_results) - paths.get_path_for_destination(origin, destination, iteration, traffic_class_id) - """ + """Class for accessing path files optionally generated during assignment.""" def __init__(self, table_name: str, project=None) -> None: """ diff --git a/aequilibrae/paths/graph.py b/aequilibrae/paths/graph.py index d3195bcb6..77676a424 100644 --- a/aequilibrae/paths/graph.py +++ b/aequilibrae/paths/graph.py @@ -42,11 +42,12 @@ class GraphBase(ABC): # noqa: B024 - dead end removal. Link contraction creates a topological equivalent graph by contracting sequences of links between nodes - with degrees of two. This compresses long streams of links, such as along highways or curved roads, into single links. + with degrees of two. This compresses long streams of links, such as along highways or curved roads, into + single links. - Dead end removal attempts to remove dead ends and fish spines from the network. It does this based on the observation - that in a graph with non-negative weights a dead end will only ever appear in the results of a short(est) path if the - origin or destination is present within that dead end. + Dead end removal attempts to remove dead ends and fish spines from the network. It does this based on the + observation that in a graph with non-negative weights a dead end will only ever appear in the results of a + short(est) path if the origin or destination is present within that dead end. Dead end removal is applied before link contraction and does not create a strictly topological equivalent graph, however, all centroids are preserved. @@ -416,7 +417,7 @@ def save_to_disk(self, filename: str) -> None: Saves graph to disk :Arguments: - **filename** (:obj:`str`): Path to file. Usual file extension is *aeg* + **filename** (:obj:`str`): Path to file. Usual file extension is 'aeg' """ mygraph = {} mygraph["description"] = self.description @@ -536,21 +537,30 @@ def save_compressed_correspondence(self, path, mode_name, mode_id): def create_compressed_link_network_mapping(self): """ - Create two arrays providing a mapping of compressed id to link id. + Create three arrays providing a mapping of compressed ID to link ID. - Uses sparse compression. Index ``idx`` by the by compressed id and compressed id + 1, the + Uses sparse compression. Index 'idx' by the by compressed ID and compressed ID + 1, the network IDs are then in the range ``idx[id]:idx[id + 1]``. + Links not in the compressed graph are not contained within the 'data' array. + .. code-block:: python - >>> idx, data = graph.compressed_link_network_mapping - >>> data[idx[id]:idx[id + 1]] # ==> Slice of network ID's corresponding to the compressed ID + >>> project = create_example(project_path) + + >>> project.network.build_graphs() + + >>> graph = project.network.graphs['c'] + >>> graph.prepare_graph(np.arange(1,25)) - Links not in the compressed graph are not contained within the ``data`` array. + >>> idx, data, node_mapping = graph.create_compressed_link_network_mapping() :Returns: **idx** (:obj:`np.array`): index array for ``data`` + **data** (:obj:`np.array`): array of link ids + + **node_mapping**: (:obj:`np.array`): array of node_mapping ids """ return create_compressed_link_network_mapping(self) diff --git a/aequilibrae/paths/network_skimming.py b/aequilibrae/paths/network_skimming.py index 578e7d894..a1bcd75c7 100644 --- a/aequilibrae/paths/network_skimming.py +++ b/aequilibrae/paths/network_skimming.py @@ -25,10 +25,9 @@ class NetworkSkimming: .. code-block:: python - >>> from aequilibrae import Project >>> from aequilibrae.paths.network_skimming import NetworkSkimming - >>> project = Project.from_path("/tmp/test_project") + >>> project = create_example(project_path) >>> network = project.network >>> network.build_graphs() @@ -48,10 +47,10 @@ class NetworkSkimming: >>> matrix = skm.results.skims # Or you can save the results to disk - >>> skm.save_to_project('/tmp/skimming result.omx') + >>> skm.save_to_project(os.path.join(project_path, 'matrices/skimming_result.omx')) # Or specify the AequilibraE's matrix file format - >>> skm.save_to_project('skimming result', 'aem') + >>> skm.save_to_project(os.path.join(project_path, 'matrices/skimming_result.aem'), 'aem') >>> project.close() """ @@ -132,7 +131,7 @@ def save_to_project(self, name: str, format="omx", project=None) -> None: :Arguments: **name** (:obj:`str`): Name of the matrix. Same value for matrix record name and file (plus extension) - **format** (:obj:`str`, *Optional*): File format ('aem' or 'omx'). Default is ``'omx'`` + **format** (:obj:`str`, *Optional*): File format ('aem' or 'omx'). Default is 'omx' **project** (:obj:`Project`, *Optional*): Project we want to save the results to. Defaults to the active project diff --git a/aequilibrae/paths/results/path_results.py b/aequilibrae/paths/results/path_results.py index 0f6626101..b09c6c1d7 100644 --- a/aequilibrae/paths/results/path_results.py +++ b/aequilibrae/paths/results/path_results.py @@ -15,14 +15,13 @@ class PathResults: .. code-block:: python - >>> from aequilibrae import Project >>> from aequilibrae.paths.results import PathResults - >>> proj = Project.from_path("/tmp/test_project") - >>> proj.network.build_graphs() + >>> project = create_example(project_path) + >>> project.network.build_graphs() # Mode c is car in this project - >>> car_graph = proj.network.graphs['c'] + >>> car_graph = project.network.graphs['c'] # minimize distance >>> car_graph.set_graph('distance') @@ -35,12 +34,6 @@ class PathResults: >>> res.prepare(car_graph) >>> res.compute_path(1, 17) - # res.milepost contains the milepost corresponding to each node along the path - # res.path_nodes contains the sequence of nodes that form the path - # res.path contains the sequence of links that form the path - # res.path_link_directions contains the link directions corresponding to the above links - # res.skims contain all skims requested when preparing the graph - # Update all the outputs mentioned above for destination 9. Same origin: 1 >>> res.update_trace(9) diff --git a/aequilibrae/paths/results/skim_results.py b/aequilibrae/paths/results/skim_results.py index d12ef2d1c..dac2677a9 100644 --- a/aequilibrae/paths/results/skim_results.py +++ b/aequilibrae/paths/results/skim_results.py @@ -10,14 +10,13 @@ class SkimResults: .. code-block:: python - >>> from aequilibrae import Project >>> from aequilibrae.paths.results import SkimResults - >>> proj = Project.from_path("/tmp/test_project") - >>> proj.network.build_graphs() + >>> project = create_example(project_path) + >>> project.network.build_graphs() # Mode c is car in this project - >>> car_graph = proj.network.graphs['c'] + >>> car_graph = project.network.graphs['c'] # minimize travel time >>> car_graph.set_graph('free_flow_time') @@ -28,7 +27,7 @@ class SkimResults: >>> res = SkimResults() >>> res.prepare(car_graph) - >>> res.skims.export('/tmp/test_project/matrix.aem') + >>> res.skims.export(os.path.join(project_path, "skim_matrices.aem")) """ def __init__(self): diff --git a/aequilibrae/paths/sub_area.py b/aequilibrae/paths/sub_area.py index 84ac6efb4..9e5d14898 100644 --- a/aequilibrae/paths/sub_area.py +++ b/aequilibrae/paths/sub_area.py @@ -26,6 +26,8 @@ def __init__( the Graph object, demand matrix, and a GeoDataFrame whose geometry union represents the desired sub-area. Perform a route choice assignment, then call the ``post_process`` method to obtain a sub-area matrix. + Check how to run sub-area analysis :ref:`here `. + :Arguments: **graph** (:obj:`Graph`): AequilibraE graph object to use @@ -34,31 +36,6 @@ def __init__( **demand** (:obj:`Union[pandas.DataFrame, AequilibraeMatrix]`): The demand matrix to provide to the route choice assignment. - - Minimal example: - - .. code-block:: python - - >>> import tempfile - >>> from aequilibrae import Project - >>> from aequilibrae.utils.create_example import create_example - >>> from aequilibrae.paths import SubAreaAnalysis - - >>> fldr = tempfile.TemporaryDirectory(suffix="-subarea") - >>> proj = create_example(fldr.name, from_model="coquimbo") - - >>> project.network.build_graphs() - >>> graph = project.network.graphs["c"] - >>> graph.network = graph.network.assign(utility=graph.network.distance * theta) - >>> graph.prepare_graph(graph.centroids) - >>> graph.set_graph("utility") - >>> graph.set_blocked_centroid_flows(False) - - >>> subarea = SubAreaAnalysis(graph, zones, mat) - >>> subarea.rc.set_choice_set_generation("lp", max_routes=5, penalty=1.02, store_results=False) - >>> subarea.rc.execute(perform_assignment=True) - >>> demand = subarea.post_process() - """ project = project if project is not None else get_active_project() self.logger = project.logger if project else logging.getLogger("aequilibrae") @@ -92,8 +69,8 @@ def post_process(self, demand_cols=None): Apply the necessary post processing to the route choice assignment select link results. :Arguments: - **demand_cols** (*Optional*: :obj:`[list[str]]`): If provided, only construct the sub-area matrix for these demand - matrices. + **demand_cols** (*Optional*: :obj:`[list[str]]`): If provided, only construct the sub-area matrix + for these demand matrices. :Returns: **sub_area_demand** (:obj:`pd.DataFrame`): A DataFrame representing the sub-area demand matrix. diff --git a/aequilibrae/paths/traffic_assignment.py b/aequilibrae/paths/traffic_assignment.py index 8510c96c5..e58186337 100644 --- a/aequilibrae/paths/traffic_assignment.py +++ b/aequilibrae/paths/traffic_assignment.py @@ -150,11 +150,9 @@ class TrafficAssignment(AssignmentBase): .. code-block:: python - >>> from aequilibrae import Project - >>> from aequilibrae.matrix import AequilibraeMatrix >>> from aequilibrae.paths import TrafficAssignment, TrafficClass - >>> project = Project.from_path("/tmp/test_project") + >>> project = create_example(project_path) >>> project.network.build_graphs() >>> graph = project.network.graphs['c'] # we grab the graph for cars @@ -164,7 +162,6 @@ class TrafficAssignment(AssignmentBase): >>> proj_matrices = project.matrices - >>> demand = AequilibraeMatrix() >>> demand = proj_matrices.get_matrix("demand_omx") # We will only assign one user class stored as 'matrix' inside the OMX file @@ -191,14 +188,12 @@ class TrafficAssignment(AssignmentBase): # And the algorithm we want to use to assign >>> assig.set_algorithm('bfw') - # Since we haven't checked the parameters file, let's make sure convergence criteria is good - >>> assig.max_iter = 1000 + >>> assig.max_iter = 10 >>> assig.rgap_target = 0.00001 >>> assig.execute() # we then execute the assignment # If you want, it is possible to access the convergence report - >>> import pandas as pd >>> convergence_report = pd.DataFrame(assig.assignment.convergence_report) # Assignment results can be viewed as a Pandas DataFrame diff --git a/aequilibrae/paths/traffic_class.py b/aequilibrae/paths/traffic_class.py index dd99feab7..6a912c597 100644 --- a/aequilibrae/paths/traffic_class.py +++ b/aequilibrae/paths/traffic_class.py @@ -68,11 +68,9 @@ class TrafficClass(TransportClassBase): .. code-block:: python - >>> from aequilibrae import Project - >>> from aequilibrae.matrix import AequilibraeMatrix >>> from aequilibrae.paths import TrafficClass - >>> project = Project.from_path("/tmp/test_project") + >>> project = create_example(project_path) >>> project.network.build_graphs() >>> graph = project.network.graphs['c'] # we grab the graph for cars @@ -82,7 +80,6 @@ class TrafficClass(TransportClassBase): >>> proj_matrices = project.matrices - >>> demand = AequilibraeMatrix() >>> demand = proj_matrices.get_matrix("demand_omx") >>> demand.computational_view(['matrix']) diff --git a/aequilibrae/project/about.py b/aequilibrae/project/about.py index 8ffc8484a..aa5a8e2df 100644 --- a/aequilibrae/project/about.py +++ b/aequilibrae/project/about.py @@ -12,9 +12,7 @@ class About: .. code-block:: python - >>> from aequilibrae import Project - - >>> project = Project.from_path("/tmp/test_project") + >>> project = create_example(project_path) # Adding a new field and saving it >>> project.about.add_info_field('my_super_relevant_field') @@ -67,12 +65,11 @@ def add_info_field(self, info_field: str) -> None: .. code-block:: python - >>> from aequilibrae import Project + >>> project = create_example(project_path) - >>> p = Project.from_path("/tmp/test_project") - >>> p.about.add_info_field('a_cool_field') - >>> p.about.a_cool_field = 'super relevant information' - >>> p.about.write_back() + >>> project.about.add_info_field('a_cool_field') + >>> project.about.a_cool_field = 'super relevant information' + >>> project.about.write_back() """ allowed = string.ascii_lowercase + "_" has_forbidden = [x for x in info_field if x not in allowed] @@ -90,11 +87,10 @@ def write_back(self): .. code-block:: python - >>> from aequilibrae import Project + >>> project = create_example(project_path) - >>> p = Project.from_path("/tmp/test_project") - >>> p.about.description = 'This is the example project. Do not use for forecast' - >>> p.about.write_back() + >>> project.about.description = 'This is the example project. Do not use for forecast' + >>> project.about.write_back() """ with commit_and_close(connect_spatialite(self.__path_to_file)) as conn: for k in self.__characteristics: diff --git a/aequilibrae/project/field_editor.py b/aequilibrae/project/field_editor.py index 488e36c63..86418d097 100644 --- a/aequilibrae/project/field_editor.py +++ b/aequilibrae/project/field_editor.py @@ -21,15 +21,13 @@ class FieldEditor: .. code-block:: python - >>> from aequilibrae import Project - - >>> proj = Project.from_path("/tmp/test_project") + >>> project = create_example(project_path) # To edit the fields of the link_types table - >>> lt_fields = proj.network.link_types.fields + >>> lt_fields = project.network.link_types.fields # To edit the fields of the modes table - >>> m_fields = proj.network.modes.fields + >>> m_fields = project.network.modes.fields Field descriptions are kept in the table *attributes_documentation* """ diff --git a/aequilibrae/project/network/link.py b/aequilibrae/project/network/link.py index 01834e6e7..0daa468d9 100644 --- a/aequilibrae/project/network/link.py +++ b/aequilibrae/project/network/link.py @@ -10,14 +10,12 @@ class Link(SafeClass): .. code-block:: python - >>> from aequilibrae import Project + >>> project = create_example(project_path) - >>> proj = Project.from_path("/tmp/test_project") - - >>> all_links = proj.network.links + >>> all_links = project.network.links # Let's get a mode to work with - >>> modes = proj.network.modes + >>> modes = project.network.modes >>> car_mode = modes.get('c') # We can just get one link in specific diff --git a/aequilibrae/project/network/link_types.py b/aequilibrae/project/network/link_types.py index 715211056..5357f0316 100644 --- a/aequilibrae/project/network/link_types.py +++ b/aequilibrae/project/network/link_types.py @@ -13,11 +13,9 @@ class LinkTypes: .. code-block:: python - >>> from aequilibrae import Project + >>> project = create_example(project_path) - >>> p = Project.from_path("/tmp/test_project") - - >>> link_types = p.network.link_types + >>> link_types = project.network.link_types # We can get a dictionary of link types in the model >>> all_link_types = link_types.all_types() diff --git a/aequilibrae/project/network/links.py b/aequilibrae/project/network/links.py index 6dd0f4974..4b3925aac 100644 --- a/aequilibrae/project/network/links.py +++ b/aequilibrae/project/network/links.py @@ -17,11 +17,9 @@ class Links(BasicTable): .. code-block:: python - >>> from aequilibrae import Project + >>> project = create_example(project_path) - >>> proj = Project.from_path("/tmp/test_project") - - >>> all_links = proj.network.links + >>> all_links = project.network.links # We can just get one link in specific >>> link = all_links.get(1) diff --git a/aequilibrae/project/network/modes.py b/aequilibrae/project/network/modes.py index 7fd12f77d..30f2a9630 100644 --- a/aequilibrae/project/network/modes.py +++ b/aequilibrae/project/network/modes.py @@ -12,11 +12,9 @@ class Modes: .. code-block:: python - >>> from aequilibrae import Project + >>> project = create_example(project_path) - >>> p = Project.from_path("/tmp/test_project") - - >>> modes = p.network.modes + >>> modes = project.network.modes # We can get a dictionary of all modes in the model >>> all_modes = modes.all_modes() diff --git a/aequilibrae/project/network/network.py b/aequilibrae/project/network/network.py index a6e8e058f..3fbb58ee8 100644 --- a/aequilibrae/project/network/network.py +++ b/aequilibrae/project/network/network.py @@ -138,28 +138,13 @@ def create_from_osm( .. code-block:: python - >>> from aequilibrae import Project - - >>> p = Project() - >>> p.new("/tmp/new_project") - - # We now choose a different overpass endpoint (say a deployment in your local network) - >>> par = Parameters() - >>> par.parameters['osm']['overpass_endpoint'] = "http://192.168.1.234:5678/api" - - # Because we have our own server, we can set a bigger area for download (in M2) - >>> par.parameters['osm']['max_query_area_size'] = 10000000000 - - # And have no pause between successive queries - >>> par.parameters['osm']['sleeptime'] = 0 - - # Save the parameters to disk - >>> par.write_back() + >>> project = Project() + >>> project.new(project_path) # Now we can import the network for any place we want - # p.network.create_from_osm(place_name="my_beautiful_hometown") + >>> project.network.create_from_osm(place_name="my_beautiful_hometown") # doctest: +SKIP - >>> p.close() + >>> project.close() """ if self.count_links() > 0: @@ -293,15 +278,14 @@ def build_graphs(self, fields: list = None, modes: list = None) -> None: **modes** (:obj:`list`, *Optional*): When working with very large graphs with large number of fields in the database, it may be useful to generate only those we need - To use the *fields* parameter, a minimalistic option is the following + To use the 'fields' parameter, a minimalistic option is the following .. code-block:: python - >>> from aequilibrae import Project + >>> project = create_example(project_path) - >>> p = Project.from_path("/tmp/test_project") >>> fields = ['distance'] - >>> p.network.build_graphs(fields, modes = ['c', 'w']) + >>> project.network.build_graphs(fields, modes = ['c', 'w']) """ from aequilibrae.paths import Graph diff --git a/aequilibrae/project/network/node.py b/aequilibrae/project/network/node.py index 71145372f..dc181598f 100644 --- a/aequilibrae/project/network/node.py +++ b/aequilibrae/project/network/node.py @@ -11,12 +11,11 @@ class Node(SafeClass): .. code-block:: python - >>> from aequilibrae import Project >>> from shapely.geometry import Point - >>> proj = Project.from_path("/tmp/test_project") + >>> project = create_example(project_path) - >>> all_nodes = proj.network.nodes + >>> all_nodes = project.network.nodes # We can just get one link in specific >>> node1 = all_nodes.get(7) diff --git a/aequilibrae/project/network/nodes.py b/aequilibrae/project/network/nodes.py index 7e045b2fe..540e1211d 100644 --- a/aequilibrae/project/network/nodes.py +++ b/aequilibrae/project/network/nodes.py @@ -17,11 +17,9 @@ class Nodes(BasicTable): .. code-block:: python - >>> from aequilibrae import Project + >>> project = create_example(project_path) - >>> proj = Project.from_path("/tmp/test_project") - - >>> all_nodes = proj.network.nodes + >>> all_nodes = project.network.nodes # We can just get one link in specific >>> node = all_nodes.get(21) diff --git a/aequilibrae/project/network/period.py b/aequilibrae/project/network/period.py index 30568199b..ea811d924 100644 --- a/aequilibrae/project/network/period.py +++ b/aequilibrae/project/network/period.py @@ -8,23 +8,15 @@ class Period(SafeClass): .. code-block:: python - >>> from aequilibrae import Project + >>> project = create_example(project_path, "coquimbo") - >>> proj = Project.from_path("/tmp/test_project") - - >>> all_periods = proj.network.periods + >>> all_periods = project.network.periods # We can just get one link in specific >>> period1 = all_periods.get(1) # We can find out which fields exist for the period >>> which_fields_do_we_have = period1.data_fields() - - # It succeeds if the period_id already does not exist - >>> period1.renumber(998877) - - # We can just save the period - >>> period1.save() """ def __init__(self, dataset, project): diff --git a/aequilibrae/project/network/periods.py b/aequilibrae/project/network/periods.py index bd251c51e..10f80f181 100644 --- a/aequilibrae/project/network/periods.py +++ b/aequilibrae/project/network/periods.py @@ -15,14 +15,12 @@ class Periods(BasicTable): .. code-block:: python - >>> from aequilibrae import Project + >>> project = create_example(project_path, "coquimbo") - >>> proj = Project.from_path("/tmp/test_project") - - >>> all_periods = proj.network.periods + >>> all_periods = project.network.periods # We can just get one link in specific - >>> period = all_periods.get(21) + >>> period = all_periods.get(1) # We can save changes for all periods we have edited so far >>> all_periods.save() diff --git a/aequilibrae/project/project.py b/aequilibrae/project/project.py index 9c6fe4f52..2830d4e67 100644 --- a/aequilibrae/project/project.py +++ b/aequilibrae/project/project.py @@ -26,25 +26,14 @@ class Project: .. code-block:: python :caption: Create Project - >>> newfile = Project() - >>> newfile.new('/tmp/new_project') + >>> new_project = Project() + >>> new_project.new(project_path) .. code-block:: python :caption: Open Project - >>> from aequilibrae.project import Project - - >>> existing = Project() - >>> existing.open('/tmp/test_project') - - >>> #Let's check some of the project's properties - >>> existing.network.list_modes() - ['M', 'T', 'b', 'c', 't', 'w'] - >>> existing.network.count_links() - 76 - >>> existing.network.count_nodes() - 24 - + >>> existing_project = Project() + >>> existing_project.open(project_path) """ def __init__(self): diff --git a/aequilibrae/project/zoning.py b/aequilibrae/project/zoning.py index e02c335b7..1b34c3424 100644 --- a/aequilibrae/project/zoning.py +++ b/aequilibrae/project/zoning.py @@ -23,9 +23,7 @@ class Zoning(BasicTable): .. code-block:: python - >>> from aequilibrae import Project - - >>> project = Project.from_path("/tmp/test_project") + >>> project = create_example(project_path, "coquimbo") >>> zoning = project.zoning diff --git a/aequilibrae/transit/lib_gtfs.py b/aequilibrae/transit/lib_gtfs.py index 502e5fc54..efe97a08e 100644 --- a/aequilibrae/transit/lib_gtfs.py +++ b/aequilibrae/transit/lib_gtfs.py @@ -131,7 +131,8 @@ def map_match(self, route_types=[3]) -> None: # noqa: B006 Defaults to map-matching Bus routes (type 3) only. - For a reference of route types, see https://developers.google.com/transit/gtfs/reference#routestxt + For a reference of route types, see the inputs for + `route_type here `_. :Arguments: **route_types** (:obj:`List[int]` or :obj:`Tuple[int]`): Default is [3], for bus only diff --git a/aequilibrae/transit/transit.py b/aequilibrae/transit/transit.py index cadce8f73..fd19a72d9 100644 --- a/aequilibrae/transit/transit.py +++ b/aequilibrae/transit/transit.py @@ -131,15 +131,15 @@ def build_pt_preload(self, start: int, end: int, inclusion_cond: str = "start") .. code-block:: python - >>> from aequilibrae import Project - >>> from aequilibrae.utils.create_example import create_example + >>> project = create_example(project_path, "coquimbo") - >>> proj = create_example(str(tmp_path / "test_traffic_assignment"), from_model="coquimbo") + >>> project.network.build_graphs() - >>> start = int(6.5 * 60 * 60) # 6.30am - >>> end = int(8.5 * 60 * 60) # 8.30 am + >>> start = int(6.5 * 60 * 60) # 6:30 am + >>> end = int(8.5 * 60 * 60) # 8:30 am - >>> preload = proj.transit.build_pt_preload(start, end) + >>> transit = Transit(project) + >>> preload = transit.build_pt_preload(start, end) """ return pd.read_sql(self.__build_pt_preload_sql(start, end, inclusion_cond), self.pt_con) diff --git a/conftest.py b/conftest.py index 5bdba433c..6098953a1 100644 --- a/conftest.py +++ b/conftest.py @@ -8,6 +8,9 @@ import pytest +import pandas as pd +import numpy as np + from aequilibrae import Project from aequilibrae.project.database_connection import database_connection from aequilibrae.transit import Transit @@ -101,6 +104,12 @@ def transit_conn(create_gtfs_project): @pytest.fixture(autouse=True) -def doctest_fixtures(doctest_namespace, tmp_path, create_path): - doctest_namespace["tmp_path"] = tmp_path - doctest_namespace["tmp_path_empty"] = str(create_path) +def doctest_fixtures(doctest_namespace, create_path, tmp_path_factory): + doctest_namespace["project_path"] = str(create_path) + doctest_namespace["my_folder_path"] = tmp_path_factory.mktemp(uuid.uuid4().hex) + doctest_namespace["create_example"] = create_example + doctest_namespace["Project"] = Project + + doctest_namespace["os"] = os + doctest_namespace["pd"] = pd + doctest_namespace["np"] = np diff --git a/docs/create_docs_data.py b/docs/create_docs_data.py index 7138e343f..a64c10be4 100644 --- a/docs/create_docs_data.py +++ b/docs/create_docs_data.py @@ -1,4 +1,3 @@ -import os import sys from pathlib import Path @@ -8,22 +7,9 @@ from aequilibrae.utils.create_example import create_example -project = create_example("/tmp/test_project") -project.close() -project = create_example("/tmp/test_project_ipf") -project.close() -project = create_example("/tmp/test_project_gc") -project.close() -project = create_example("/tmp/test_project_ga") -project.close() project = create_example("/tmp/accessing_sfalls_data") project.close() project = create_example("/tmp/accessing_nauru_data", "nauru") project.close() project = create_example("/tmp/accessing_coquimbo_data", "coquimbo") project.close() - -# Create empty folder -if not os.path.exists("/tmp/matrix_example"): - - os.makedirs("/tmp/matrix_example") \ No newline at end of file diff --git a/docs/source/images/qgis_creating_spatial_indices.png b/docs/source/images/qgis_creating_spatial_indices.png deleted file mode 100644 index c3bf8111d..000000000 Binary files a/docs/source/images/qgis_creating_spatial_indices.png and /dev/null differ diff --git a/docs/source/modeling_with_aequilibrae/project_database/network_import_and_export.rst b/docs/source/modeling_with_aequilibrae/project_database/network_import_and_export.rst index 62ec2694f..13c6e66f4 100644 --- a/docs/source/modeling_with_aequilibrae/project_database/network_import_and_export.rst +++ b/docs/source/modeling_with_aequilibrae/project_database/network_import_and_export.rst @@ -28,53 +28,15 @@ As it happens in other cases, Python's usual implementation of SQLite is incomplete, and does not include R-Tree, a key extension used by SpatiaLite for GIS operations. -For this reason, AequilibraE's default option when importing a network from OSM -is to **NOT create spatial indices**, which renders the network consistency -triggers useless. - -If you are using a vanilla Python installation (your case if you are not sure), -you can import the network without creating indices, as shown below. - -.. code-block:: python - - from aequilibrae.project import Project - - p = Project() - p.new('path/to/project/new/folder') - p.network.create_from_osm(place_name='my favorite place') - p.conn.close() - -And then manually add the spatial index on QGIS by adding both links and nodes -layers to the canvas, and selecting properties and clicking on *create spatial* -*index* for each layer at a time. This action automatically saves the spatial -indices to the sqlite database. - -.. image:: ../../images/qgis_creating_spatial_indices.png - :align: center - :alt: Adding Spatial indices with QGIS - -If you are an expert user and made sure your Python installation was compiled -against a complete SQLite set of extensions, then go ahead an import the network -with the option for creating such indices. - -.. code-block:: python - - from aequilibrae.project import Project - - p = Project() - p.new('path/to/project/new/folder/') - p.network.create_from_osm(place_name='my favorite place', spatial_index=True) - p.conn.close() - If you want to learn a little more about this topic, you can access this `blog post `_ or check out the SQLite page on `R-Tree `_. -If you want to take a stab at solving your SQLite/SpatiaLite problem -permanently, take a look at this -`other blog post `_. -Please also note that the network consistency triggers will NOT work before -spatial indices have been created and/or if the editing is being done on a +This limitation issue is solved when installing SpatiaLite, as shown +in :ref:`the dependencies page `. + +Please also note that AequilibraE's network consistency triggers **will NOT work** +before spatial indices have been created and/or if the editing is being done on a platform that does not support both R-Tree and SpatiaLite. .. seealso:: diff --git a/docs/source/modeling_with_aequilibrae/project_pieces/accessing_project_data.rst b/docs/source/modeling_with_aequilibrae/project_pieces/accessing_project_data.rst index 3aa665b89..ce3e505ad 100644 --- a/docs/source/modeling_with_aequilibrae/project_pieces/accessing_project_data.rst +++ b/docs/source/modeling_with_aequilibrae/project_pieces/accessing_project_data.rst @@ -15,11 +15,9 @@ Each item in the 'links' table is a ``Link`` object. .. code-block:: python - >>> from aequilibrae import Project >>> from shapely.geometry import LineString - >>> project = Project() - >>> project.open("/tmp/accessing_coquimbo_data") + >>> project = create_example(project_path, "coquimbo") >>> project_links = project.network.links diff --git a/docs/source/modeling_with_aequilibrae/project_pieces/aequilibrae_matrix.rst b/docs/source/modeling_with_aequilibrae/project_pieces/aequilibrae_matrix.rst index ab1ac26f1..a7a0ea31f 100644 --- a/docs/source/modeling_with_aequilibrae/project_pieces/aequilibrae_matrix.rst +++ b/docs/source/modeling_with_aequilibrae/project_pieces/aequilibrae_matrix.rst @@ -21,11 +21,9 @@ There are three ways of creating an ``AequilibraeMatrix``: .. code-block:: python - >>> import numpy as np - >>> from os.path import join >>> from aequilibrae.matrix import AequilibraeMatrix - >>> file = "/tmp/matrix_example/path_to_my_matrix.aem" + >>> file = os.path.join(my_folder_path, "path_to_my_matrix.aem") >>> num_zones = 5 >>> index = np.arange(1, 6, dtype=np.int32) >>> mtx = np.ones((5, 5), dtype=np.float32) @@ -81,16 +79,16 @@ for CSV file, in which all cores will be exported as separate columns in the out .. code-block:: python - >>> mat.export('/tmp/matrix_example/my_new_omx_file.omx') + >>> mat.export(os.path.join(my_folder_path, 'my_new_omx_file.omx')) - >>> mat.export('/tmp/matrix_example/my_new_csv_file.csv') + >>> mat.export(os.path.join(my_folder_path, 'my_new_csv_file.csv')) The ``export`` method also allows you to change your mind and save your AequilibraE matrix into an AEM file, if it's only in memory. .. code-block:: python - >>> mat.export('/tmp/matrix_example/my_new_aem_file.aem') + >>> mat.export(os.path.join(my_folder_path, 'my_new_aem_file.aem')) .. is there a better name rather than error? @@ -107,7 +105,7 @@ AequilibraE matrices in disk can be reused and loaded once again. .. code-block:: python >>> mat = AequilibraeMatrix() - >>> mat.load('/tmp/matrix_example/my_new_aem_file.aem') + >>> mat.load(os.path.join(my_folder_path, 'my_new_aem_file.aem')) >>> mat.get_matrix("only_ones") # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE memmap([[1., 1., 1., 1., 1.], @@ -149,8 +147,8 @@ Creating an AequilibraE matrix from an OMX file is pretty straightforward. .. code-block:: python - >>> file_path = "/tmp/matrix_example/path_to_new_matrix.aem" - >>> omx_path = '/tmp/matrix_example/my_new_omx_file.omx' + >>> file_path = os.path.join(my_folder_path, "path_to_new_matrix.aem") + >>> omx_path = os.path.join(my_folder_path, "my_new_omx_file.omx") >>> omx_mat = AequilibraeMatrix() >>> omx_mat.create_from_omx(file_path, omx_path) diff --git a/docs/source/modeling_with_aequilibrae/project_pieces/project_components.rst b/docs/source/modeling_with_aequilibrae/project_pieces/project_components.rst index 6f2b50798..4dc55230c 100644 --- a/docs/source/modeling_with_aequilibrae/project_pieces/project_components.rst +++ b/docs/source/modeling_with_aequilibrae/project_pieces/project_components.rst @@ -39,7 +39,6 @@ edit the existing ones as necessary, but everytime you add or modify a field, yo this information, otherwise it will be lost. .. doctest:: - >>> from aequilibrae import Project >>> project = Project() >>> project.open("/tmp/accessing_sfalls_data") diff --git a/docs/source/modeling_with_aequilibrae/static_traffic_assignment/assignment_mechanics.rst b/docs/source/modeling_with_aequilibrae/static_traffic_assignment/assignment_mechanics.rst index 96ee9ed30..f6e9bebd6 100644 --- a/docs/source/modeling_with_aequilibrae/static_traffic_assignment/assignment_mechanics.rst +++ b/docs/source/modeling_with_aequilibrae/static_traffic_assignment/assignment_mechanics.rst @@ -6,12 +6,12 @@ always a little different in each platform, and in AequilibraE is not different. The complexity in computing paths through a network comes from the fact that transportation models usually house networks for multiple transport modes, so -the toads (links) available for a passenger car may be different than those available +the loads (links) available for a passenger car may be different than those available for a heavy truck, as it happens in practice. -For this reason, all path computation in AequilibraE happens through **Graph** objects. +For this reason, all path computation in AequilibraE happens through Graph objects. While users can operate models by simply selecting the mode they want AequilibraE to -create graphs for, **Graph** objects can also be manipulated in memory or even created +create graphs for, Graph objects can also be manipulated in memory or even created from networks that are :ref:`NOT housed inside an AequilibraE model `. .. _aequilibrae-graphs: @@ -21,22 +21,22 @@ AequilibraE Graphs As mentioned above, AequilibraE's graphs are the backbone of path computation, skimming and Traffic Assignment. Besides handling the selection of links available to -each mode in an AequilibraE model, **Graphs** also handle the existence of bi-directional +each mode in an AequilibraE model, graphs also handle the existence of bi-directional links with direction-specific characteristics (e.g. speed limit, congestion levels, tolls, etc.). -The **Graph** object is rather complex, but the difference between the physical links and -those that are available two class member variables consisting of Pandas DataFrames, the +The Graph object is rather complex, but the difference between the graph and the physical +links are the availability of two class member variables consisting of Pandas DataFrames: the **network** and the **graph**. .. code-block:: python - from aequilibrae.paths import Graph + >>> from aequilibrae.paths import Graph - g = Graph() + >>> g = Graph() - # g.network - # g.graph + >>> g.network # doctest: +SKIP + >>> g.graph # doctest: +SKIP Directionality ^^^^^^^^^^^^^^ @@ -47,7 +47,7 @@ field *direction*, where -1 and 1 denote only BA and AB traversal respectively a bi-directionality. Direction-specific fields must be coded in fields **_AB** and **_BA**, where the name of -the field in the *graph* will be equal to the prefix of the directional fields. For example: +the field in the graph will be equal to the prefix of the directional fields. For example: The fields **free_flow_travel_time_AB** and **free_flow_travel_time_BA** provide the same metric (*free_flow_travel_time*) for each of the directions of a link, and the field of @@ -62,12 +62,10 @@ or when using AequilibraE in anger, as much of the setup is done by default. .. code-block:: python - from aequilibrae import Project + >>> project = create_example(project_path, "coquimbo") - project = Project.from_path("/tmp/test_project") - project.network.build_graphs(modes=["c"]) # We build the graph for cars only - - graph = project.network.graphs['c'] # we grab the graph for cars + >>> project.network.build_graphs() # We build the graph for all modes + >>> graph = project.network.graphs['c'] # we grab the graph for cars Manipulating graphs in memory ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -79,50 +77,49 @@ API is a method call for excluding one or more links from the Graph, **which is .. code-block:: python - graph.exclude_links([123, 975]) + >>> graph.exclude_links([123, 975]) More sophisticated graph editing is also possible, but it is recommended that changes to be made in the network DataFrame. For example: .. code-block:: python - graph.network.loc[graph.network.link_type="highway", "speed_AB"] = 100 - graph.network.loc[graph.network.link_type="highway", "speed_BA"] = 100 + # We can add fields to our graph + >>> graph.network["link_type"] = project.network.links.data["link_type"] - graph.prepare_graph(graph.centroids) - if graph.skim_fields: - graph.set_skimming(graph.skim_fields) + # And manipulate them + >>> graph.network.loc[graph.network.link_type == "motorway", "speed_ab"] = 100 + >>> graph.network.loc[graph.network.link_type == "motorway", "speed_ba"] = 100 Skimming settings ^^^^^^^^^^^^^^^^^ Skimming the field of a graph when computing shortest path or performing traffic assignment must be done by setting the skimming fields in the -**Graph** object, and there are no limits (other than memory) to the number +Graph object, and there are no limits (other than memory) to the number of fields that can be skimmed. - .. code-block:: python - graph.set_skimming(["tolls", "distance", "free_flow_travel_time"]) + >>> graph.set_skimming(["distance", "travel_time"]) Setting centroids ^^^^^^^^^^^^^^^^^ -Like other elements of the AequilibraE **Graph**, the user can also manipulate the -set of nodes interpreted by the software as centroids in the **Graph** itself. +Like other elements of the AequilibraE Graph, the user can also manipulate the +set of nodes interpreted by the software as centroids in the Graph itself. This brings the advantage of allowing the user to perform assignment of partial matrices, matrices of travel between arbitrary network nodes and to skim the network for an arbitrary number of centroids in parallel, which can be useful when using AequilibraE as part of more general analysis pipelines. As seen above, this is also necessary when the network has been manipulated in memory. -When setting regular network nodes as centroids, the user should take care in -not blocking flows through "centroids". +**When setting regular network nodes as centroids, the user should take care in +not blocking flows through "centroids".** .. code-block:: python - graph.prepare_graph(np.array([13, 169, 2197, 28561, 371293], np.int)) - graph.set_blocked_centroid_flows(False) + >>> graph.prepare_graph(np.array([13, 169, 2197, 28561, 37123], np.int32)) + >>> graph.set_blocked_centroid_flows(False) .. seealso:: @@ -133,249 +130,4 @@ not blocking flows through "centroids". .. _traffic_assignment_procedure: -Traffic Assignment Procedure -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Along with a network data model, traffic assignment is the most technically -challenging portion to develop in a modeling platform, especially if you want it -to be **FAST**. In AequilibraE, we aim to make it as fast as possible, without -making it overly complex to use, develop and maintain (we know *complex* is -subjective). - -Below we detail the components that go into performing traffic assignment, but for -a comprehensive use case for the traffic assignment module, please see the complete -application in :ref:`this example `. - -Traffic Assignment Class -^^^^^^^^^^^^^^^^^^^^^^^^ - -Traffic assignment is organized within a object introduced on version 0.6.1 of -AequilibraE, and includes a small list of member variables which should be populated -by the user, providing a complete specification of the assignment procedure: - -* **classes**: List of objects :ref:`assignment_class_object` , each of which - are a completely specified traffic class - -* **vdf**: The Volume delay function (VDF) to be used - -* **vdf_parameters**: The parameters to be used in the volume delay function, - other than volume, capacity and free flow time - -* **time_field**: The field of the graph that corresponds to **free-flow** - **travel time**. The procedure will collect this information from the graph - associated with the first traffic class provided, but will check if all graphs - have the same information on free-flow travel time - -* **capacity_field**: The field of the graph that corresponds to **link** - **capacity**. The procedure will collect this information from the graph - associated with the first traffic class provided, but will check if all graphs - have the same information on capacity - -* **algorithm**: The assignment algorithm to be used. (e.g. "all-or-nothing", "bfw") - -Assignment parameters such as maximum number of iterations and target relative -gap come from the global software parameters, that can be set using the -:ref:`parameters_file` . - -There are also some strict technical requirements for formulating the -multi-class equilibrium assignment as an unconstrained convex optimization problem, -as we have implemented it. These requirements are loosely listed in -:ref:`technical_requirements_multi_class` . - -If you want to see the assignment log on your terminal during the assignment, -please look in the :ref:`logging to terminal ` example. - -To begin building the assignment it is easy: - -.. code-block:: python - - from aequilibrae.paths import TrafficAssignment - - assig = TrafficAssignment() - -.. seealso:: - - * :func:`aequilibrae.paths.TrafficAssignment` - Class documentation - -.. _assignment_class_object: - -Traffic class -^^^^^^^^^^^^^ - -The Traffic class object holds all the information pertaining to a specific -traffic class to be assigned. There are three pieces of information that are -required in the instantiation of this class: - -* **name** - Name of the class. Unique among all classes used in a multi-class - traffic assignment - -* **graph** - It is the Graph object corresponding to that particular traffic class/ - mode - -* **matrix** - It is the AequilibraE matrix with the demand for that traffic class, - but which can have an arbitrary number of user-classes, setup as different - layers of the matrix object - -* **pce** - The passenger-car equivalent is the standard way of modeling - multi-class traffic assignment equilibrium in a consistent manner (see [3]_ for - the technical detail), and it is set to 1 by default. If the **pce** for a - certain class should be different than one, one can make a quick method call. - -* **fixed_cost** - In case there are fixed costs associated with the traversal of - links in the network, the user can provide the name of the field in the graph - that contains that network. - -* **vot** - Value-of-Time (VoT) is the mechanism to bring time and monetary - costs into a consistent basis within a generalized cost function. In the event - that fixed cost is measured in the same unit as free-flow travel time, then - **vot** must be set to 1.0, and can be set to the appropriate value (1.0, - value-of-timeIf the **vot** or whatever conversion factor is appropriate) with - a method call. - -.. code-block:: python - - from aequilibrae.paths import TrafficClass - - tc_car = TrafficClass("car", graph_car, matrix_car) - tc_truck = TrafficClass("truck", graph_truck, matrix_truck) - - tc_truck.set_pce(2.5) - tc_truck.set_fixed_cost("truck_toll") - tc_truck.set_vot(0.35) - - # Add traffic classes to the assignment instance - assig.set_classes([tc_car, tc_truck]) - - # To add only one class to the assignment instance - # assig.add_class(tc_truck) - -.. seealso:: - - * :func:`aequilibrae.paths.TrafficClass` - Class documentation - -Volume Delay Function -^^^^^^^^^^^^^^^^^^^^^ - -For now, the only VDF functions available in AequilibraE are - -* BPR [1]_ - -.. math:: CongestedTime_{i} = FreeFlowTime_{i} * (1 + \alpha * (\frac{Volume_{i}}{Capacity_{i}})^\beta) - -* Spiess' conical [2]_ - -.. math:: CongestedTime_{i} = FreeFlowTime_{i} * (2 + \sqrt[2][\alpha^2*(1- \frac{Volume_{i}}{Capacity_{i}})^2 + \beta^2] - \alpha *(1-\frac{Volume_{i}}{Capacity_{i}})-\beta) - -* and French INRETS (alpha < 1) - -Before capacity - -.. math:: CongestedTime_{i} = FreeFlowTime_{i} * \frac{1.1- (\alpha *\frac{Volume_{i}}{Capacity_{i}})}{1.1-\frac{Volume_{i}}{Capacity_{i}}} - -and after capacity - -.. math:: CongestedTime_{i} = FreeFlowTime_{i} * \frac{1.1- \alpha}{0.1} * (\frac{Volume_{i}}{Capacity_{i}})^2 - -More functions will be added as needed/requested/possible. - -Setting the volume delay function is one of the first things you should do after -instantiating an assignment problem in AequilibraE, and it is as simple as: - -The implementation of the VDF functions in AequilibraE is written in Cython and -fully multi-threaded, and therefore descent methods that may evaluate such -function multiple times per iteration should not become unecessarily slow, -especially in modern multi-core systems. - -Setting VDF Parameters -"""""""""""""""""""""" - -Parameters for VDF functions can be passed as a fixed value to use for all -links, or as graph fields. As it is the case for the travel time and capacity -fields, VDF parameters need to be consistent across all graphs. - -Because AequilibraE supports different parameters for each link, its -implementation is the most general possible while still preserving the desired -properties for multi-class assignment, but the user needs to provide individual -values for each link **OR** a single value for the entire network. - -Setting the VDF parameters should be done **AFTER** setting the VDF function of -choice and adding traffic classes to the assignment, or it will **fail**. - -.. code-block:: python - - assig.set_vdf('BPR') - - # To set the parameters using a field that exists in the graph, just pass - # them as parameters - assig.set_vdf_parameters({"alpha": "alphas", "beta": "betas"}) - - # Or to pass global values, it is simply a matter of doing the following: - assig.set_vdf_parameters({"alpha": 0.15, "beta": 4}) - -.. seealso:: - - * :func:`aequilibrae.paths.VDF` - Class documentation - -Setting final parameters -^^^^^^^^^^^^^^^^^^^^^^^^ - -There are still three parameters missing for the assignment. - -* Capacity field -* Travel time field -* Equilibrium algorithm to use - -.. code-block:: python - - assig.set_capacity_field("capacity") - assig.set_time_field("free_flow_time") - assig.set_algorithm(algorithm) - -Setting Preloads -^^^^^^^^^^^^^^^^ - -We can also optionally include a preload vector for constant flows which are not -being otherwise modelled. For example, this can be used to account for scheduled -public transport vehicles, adding an equivalent load to each link along the route accordingly. -AequilibraE supports various conditions for which PT trips to include in the preload, -and allows the user to specify the PCE for each type of vehicle in the public transport -network. - -To create a preload for public transport vehicles operating between 8am to -10am, do the following: - -.. code-block:: python - - # Times are specified in seconds from midnight - transit = Transit(project) - preload = transit.build_pt_preload(start=8*3600, end=10*3600) - -Next, add the preload to the assignment. - -.. code-block:: python - - assig.add_preload(preload, 'PT_vehicles') - -Executing an Assignment -^^^^^^^^^^^^^^^^^^^^^^^ - -Finally, one can execute assignment: - -.. code-block:: python - - assig.execute() - -:ref:`convergence_criteria` is discussed in a different section. - -.. [1] Hampton Roads Transportation Planning Organization, Regional Travel Demand Model V2 (2020). - Available in: https://www.hrtpo.org/uploads/docs/2020_HamptonRoads_Modelv2_MethodologyReport.pdf - -.. [2] Spiess H. (1990) "Technical Note—Conical Volume-Delay Functions."Transportation Science, 24(2): 153-158. - Available in: https://doi.org/10.1287/trsc.24.2.153 - -.. [3] Zill, J., Camargo, P., Veitch, T., Daisy,N. (2019) "Toll Choice and Stochastic User Equilibrium: - Ticking All the Boxes", Transportation Research Record, 2673(4):930-940. - Available in: https://doi.org/10.1177%2F0361198119837496 \ No newline at end of file +.. include:: traffic_assignment_procedure.rst \ No newline at end of file diff --git a/docs/source/modeling_with_aequilibrae/static_traffic_assignment/traffic_assignment_procedure.rst b/docs/source/modeling_with_aequilibrae/static_traffic_assignment/traffic_assignment_procedure.rst new file mode 100644 index 000000000..6929bd7f9 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/static_traffic_assignment/traffic_assignment_procedure.rst @@ -0,0 +1,275 @@ +Traffic Assignment Procedure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Along with a network data model, traffic assignment is the most technically +challenging portion to develop in a modeling platform, especially if you want it +to be **FAST**. In AequilibraE, we aim to make it as fast as possible, without +making it overly complex to use, develop and maintain (we know *complex* is +subjective). + +Below we detail the components that go into performing traffic assignment, but for +a comprehensive use case for the traffic assignment module, please see the complete +application in :ref:`this example `. + +Traffic Assignment Class +^^^^^^^^^^^^^^^^^^^^^^^^ + +Traffic assignment is organized within a object introduced on version 0.6.1 of +AequilibraE, and includes a small list of member variables which should be populated +by the user, providing a complete specification of the assignment procedure: + +* **classes**: List of objects :ref:`assignment_class_object`, each of which + are a completely specified Traffic Class + +* **vdf**: The Volume delay function (VDF) to be used + +* **vdf_parameters**: The parameters to be used in the volume delay function, + other than volume, capacity and free flow time + +* **time_field**: The field of the graph that corresponds to **free-flow** + **travel time**. The procedure will collect this information from the graph + associated with the first traffic class provided, but will check if all graphs + have the same information on free-flow travel time + +* **capacity_field**: The field of the graph that corresponds to **link** + **capacity**. The procedure will collect this information from the graph + associated with the first traffic class provided, but will check if all graphs + have the same information on capacity + +* **algorithm**: The assignment algorithm to be used. (e.g. "all-or-nothing", "bfw") + +Assignment parameters such as maximum number of iterations and target relative +gap come from the global software parameters, that can be set using the +:ref:`parameters_file`. + +There are also some strict technical requirements for formulating the +multi-class equilibrium assignment as an unconstrained convex optimization problem, +as we have implemented it. These requirements are loosely listed in +:ref:`technical_requirements_multi_class` . + +If you want to see the assignment log on your terminal during the assignment, +please look in the :ref:`logging to terminal ` example. + +To begin building the assignment it is easy: + +.. code-block:: python + + >>> from aequilibrae.paths import TrafficAssignment + + >>> project = create_example(project_path) + + >>> assig = TrafficAssignment() + + # Set the assignment parameters + >>> assig.max_iter = 10 + >>> assig.rgap_target = 0.01 + +.. seealso:: + + * :func:`aequilibrae.paths.TrafficAssignment` + Class documentation + +.. _assignment_class_object: + +Traffic class +^^^^^^^^^^^^^ + +The Traffic class object holds all the information pertaining to a specific +traffic class to be assigned. There are three pieces of information that are +required in the instantiation of this class: + +* **name** - Name of the class. Unique among all classes used in a multi-class + traffic assignment + +* **graph** - It is the Graph object corresponding to that particular traffic class/ + mode + +* **matrix** - It is the AequilibraE matrix with the demand for that traffic class, + but which can have an arbitrary number of user-classes, setup as different + layers of the matrix object + +.. code-block:: python + + >>> from aequilibrae.paths import TrafficClass + + >>> project.network.build_graphs() + + # We get the graphs for cars and trucks + >>> graph_car = project.network.graphs['c'] + >>> graph_truck = project.network.graphs['T'] + + # And also get the matrices for cars and trucks + >>> matrix_car = project.matrices.get_matrix("demand_mc") + >>> matrix_car.computational_view("car") + + >>> matrix_truck = project.matrices.get_matrix("demand_mc") + >>> matrix_truck.computational_view("trucks") + + # We create the Traffic Classes + >>> tc_car = TrafficClass("car", graph_car, matrix_car) + >>> tc_truck = TrafficClass("truck", graph_truck, matrix_truck) + +One can also edit some information related to the passenger-car equivalent, the fixed cost, +or the value of time for each traffic class. + +* **pce** - The passenger-car equivalent is the standard way of modeling + multi-class traffic assignment equilibrium in a consistent manner (see [3]_ for + the technical detail), and it is sevalue# doctest: +SKIPt to 1.0 by default. If the **pce** for a + certain class should be different than 1.0, one can make a quick method call + to set the appropriate value. + +* **fixed_cost** - In case there are fixed costs associated with the traversal of + links in the network, the user can provide the name of the field in the graph + that contains that network. + +* **vot** - Value-of-Time (VoT) is the mechanism to bring time and monetary + costs into a consistent basis within a generalized cost function. In the event + that fixed cost is measured in the same unit as free-flow travel time, then + **vot** must be set to 1.0. If the **vot** for a certain class should be different + than 1.0, one can make a quick method call to set the appropriate value. + +.. code-block:: python + + >>> tc_truck.set_pce(2.5) + >>> tc_truck.set_fixed_cost("distance") + >>> tc_truck.set_vot(0.35) + +Traffic classes must be assigned to a Traffic Assignment instance: + +.. code-block:: python + + # You can add one or more traffic classes to the assignment instance + >>> assig.add_class(tc_truck) # doctest: +SKIP + + >>> assig.set_classes([tc_car, tc_truck]) + +.. seealso:: + + * :func:`aequilibrae.paths.TrafficClass` + Class documentation + +Volume Delay Function +^^^^^^^^^^^^^^^^^^^^^ + +For now, the only VDF functions available in AequilibraE are + +* BPR [1]_ + +.. math:: CongestedTime_{i} = FreeFlowTime_{i} * (1 + \alpha * (\frac{Volume_{i}}{Capacity_{i}})^\beta) + +* Spiess' conical [2]_ + +.. math:: CongestedTime_{i} = FreeFlowTime_{i} * (2 + \sqrt[2][\alpha^2*(1- \frac{Volume_{i}}{Capacity_{i}})^2 + \beta^2] - \alpha *(1-\frac{Volume_{i}}{Capacity_{i}})-\beta) + +* and French INRETS (alpha < 1) + +Before capacity + +.. math:: CongestedTime_{i} = FreeFlowTime_{i} * \frac{1.1- (\alpha *\frac{Volume_{i}}{Capacity_{i}})}{1.1-\frac{Volume_{i}}{Capacity_{i}}} + +and after capacity + +.. math:: CongestedTime_{i} = FreeFlowTime_{i} * \frac{1.1- \alpha}{0.1} * (\frac{Volume_{i}}{Capacity_{i}})^2 + +More functions will be added as needed/requested/possible. + +Setting the volume delay function is one of the first things you should do after +instantiating an assignment problem in AequilibraE, and it is as simple as: + +.. code-block:: python + + >>> assig.set_vdf('BPR') + +The implementation of the VDF functions in AequilibraE is written in Cython and +fully multi-threaded, and therefore descent methods that may evaluate such +function multiple times per iteration should not become unecessarily slow, +especially in modern multi-core systems. + +Setting VDF Parameters +^^^^^^^^^^^^^^^^^^^^^^ + +Parameters for VDF functions can be passed as a fixed value to use for all +links, or as graph fields. As it is the case for the travel time and capacity +fields, VDF parameters need to be consistent across all graphs. + +Because AequilibraE supports different parameters for each link, its +implementation is the most general possible while still preserving the desired +properties for multi-class assignment, but the user needs to provide individual +values for each link **OR** a single value for the entire network. + +Setting the VDF parameters should be done **AFTER** setting the VDF function of +choice and adding traffic classes to the assignment, or it will **fail**. + +.. code-block:: python + + # The VDF parameters can be either an existing field in the graph, passed + # as a parameter: + >>> assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) # doctest: +SKIP + + # Or as a global value: + >>> assig.set_vdf_parameters({"alpha": 0.15, "beta": 4}) + +.. seealso:: + + * :func:`aequilibrae.paths.VDF` + Class documentation + +Setting final parameters +^^^^^^^^^^^^^^^^^^^^^^^^ + +There are still three parameters missing for the assignment. + +* Capacity field +* Travel time field +* Equilibrium algorithm to use + +.. code-block:: python + + >>> assig.set_capacity_field("capacity") + >>> assig.set_time_field("free_flow_time") + >>> assig.set_algorithm("bfw") + +Setting Preloads +^^^^^^^^^^^^^^^^ + +We can also optionally include a preload vector for constant flows which are not +being otherwise modelled. For example, this can be used to account for scheduled +public transport vehicles, adding an equivalent load to each link along the route accordingly. +AequilibraE supports various conditions for which PT trips to include in the preload, +and allows the user to specify the PCE for each type of vehicle in the public transport +network. + +To create a preload for public transport vehicles operating between 8am to +10am, do the following: + +.. code-block:: python + + >>> from aequilibrae.transit import Transit + + # Times are specified in seconds from midnight + >>> transit = Transit(project) + >>> preload = transit.build_pt_preload(start=8*3600, end=10*3600) + + # Add the preload to the assignment + >>> assig.add_preload(preload, 'PT_vehicles') # doctest: +SKIP + +Executing an Assignment +^^^^^^^^^^^^^^^^^^^^^^^ + +Finally, one can execute assignment: + +.. code-block:: python + + >>> assig.execute() + +:ref:`convergence_criteria` is discussed in a different section. + +.. [1] Hampton Roads Transportation Planning Organization, Regional Travel Demand Model V2 (2020). + Available in: https://www.hrtpo.org/uploads/docs/2020_HamptonRoads_Modelv2_MethodologyReport.pdf + +.. [2] Spiess H. (1990) "Technical Note—Conical Volume-Delay Functions."Transportation Science, 24(2): 153-158. + Available in: https://doi.org/10.1287/trsc.24.2.153 + +.. [3] Zill, J., Camargo, P., Veitch, T., Daisy,N. (2019) "Toll Choice and Stochastic User Equilibrium: + Ticking All the Boxes", Transportation Research Record, 2673(4):930-940. + Available in: https://doi.org/10.1177%2F0361198119837496 \ No newline at end of file diff --git a/docs/source/useful_information/installation.rst b/docs/source/useful_information/installation.rst index e4f641cd6..5869ae54a 100644 --- a/docs/source/useful_information/installation.rst +++ b/docs/source/useful_information/installation.rst @@ -33,7 +33,7 @@ Dependencies All of AequilibraE's dependencies are readily available from `PyPI `_ for all currently supported Python versions and major platforms. -.. _installing_spatialite_on_windows: +.. _installing_spatialite: SpatiaLite ++++++++++