diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 72b484efa..f63fe5fc2 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -34,10 +34,21 @@ jobs: - name: Compile library run: | - python3 setup.py build_ext --inplace + python setup.py build_ext --inplace - name: Check history of versions - run: python docs/website/check_documentation_versions.py + run: | + python docs/table_documentation.py + python docs/create_docs_data.py + python docs/website/check_documentation_versions.py + + - name: Test docstrings + run: | + echo "DOCUMENTATION TESTING GOES HERE" + python -m doctest -v ./aequilibrae/parameters.py + python -m doctest -v ./aequilibrae/project/about.py + python -m doctest -v ./aequilibrae/project/zoning.py + - name: Build documentation run: | diff --git a/.gitignore b/.gitignore index 9780b469c..d2631e985 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ aequilibrae/**/*.html aequilibrae/**/build/* docs/build/* docs/source/_generated/* +docs/source/project_database/* docs/source/_auto_examples/* *.cpp diff --git a/README.rst b/README.rst index c176c536e..5f499a04c 100644 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ What is available What is available only in QGIS ****************************** -Some common resources for transportation modelling are inherently visual, and therefore they make more sense if +Some common resources for transportation modeling are inherently visual, and therefore they make more sense if available within a GIS platform. For that reason, many resources are available only from AequilibraE's `QGIS plugin `_, which uses AequilibraE as its computational workhorse and also provides GUIs for most of AequilibraE's tools. Said tool @@ -85,7 +85,7 @@ Before there was AequilibraE, there was a need for something like AequilibraE ou The very early days ******************* It all started when I was a student at `UCI-ITS `_ and needed low level access to outputs of standard -algorithms used in transportation modelling (e.g. path files from traffic assignment) and had that denied by the maker +algorithms used in transportation modeling (e.g. path files from traffic assignment) and had that denied by the maker of the commercial software he normally used. There, the `first scratch of a traffic assignment procedure `_ was born. After that, there were a couple of scripts developed to implement synthetic gravity models (calibration and application) diff --git a/__version__.py b/__version__.py index 52eeb1aba..a44ec9392 100644 --- a/__version__.py +++ b/__version__.py @@ -1,5 +1,5 @@ -version = 0.8 -minor_version = "4" -release_name = "Rio de Janeiro" +version = 0.9 +minor_version = "0" +release_name = "Queluz" release_version = f"{version}.{minor_version}" diff --git a/aequilibrae/distribution/gravity_application.py b/aequilibrae/distribution/gravity_application.py index f15897dff..08bf67be1 100644 --- a/aequilibrae/distribution/gravity_application.py +++ b/aequilibrae/distribution/gravity_application.py @@ -20,118 +20,119 @@ class GravityApplication: - """Applies a synthetic gravity model + """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 + 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 - import sqlite3 + >>> import pandas as pd + >>> from aequilibrae import Project + >>> from aequilibrae.matrix import AequilibraeMatrix, AequilibraeData + >>> from aequilibrae.distribution import SyntheticGravityModel, GravityApplication - from aequilibrae.matrix import AequilibraeMatrix - from aequilibrae.matrix import AequilibraeData - - from aequilibrae.distribution import SyntheticGravityModel - from aequilibrae.distribution import GravityApplication + >>> project = Project.from_path("/tmp/test_project_ga") # We define the model we will use - model = SyntheticGravityModel() + >>> model = SyntheticGravityModel() # Before adding a parameter to the model, you need to define the model functional form - model.function = "GAMMA" # "EXPO" or "POWER" + >>> model.function = "GAMMA" # "EXPO" or "POWER" # Only the parameter(s) applicable to the chosen functional form will have any effect - model.alpha = 0.1 - model.beta = 0.0001 + >>> model.alpha = 0.1 + >>> model.beta = 0.0001 # Or you can load the model from a file - model.load('path/to/model/file') + # model.load('path/to/model/file') # We load the impedance matrix - matrix = AequilibraeMatrix() - matrix.load('path/to/impedance_matrix.aem') - matrix.computational_view(['distance']) + >>> matrix = AequilibraeMatrix() + >>> matrix.load('/tmp/test_project_ga/matrices/skims.omx') + >>> matrix.computational_view(['distance_blended']) # We create the vectors we will use - conn = sqlite3.connect('path/to/demographics/database') - query = "SELECT zone_id, population, employment FROM demographics;" - df = pd.read_sql_query(query,conn) - - index = df.zone_id.values[:] - zones = index.shape[0] + >>> query = "SELECT zone_id, population, employment FROM zones;" + >>> df = pd.read_sql(query, project.conn) + >>> df.sort_values(by="zone_id", inplace=True) # You create the vectors you would have - df = df.assign(production=df.population * 3.0) - df = df.assign(attraction=df.employment * 4.0) + >>> df = df.assign(production=df.population * 3.0) + >>> df = df.assign(attraction=df.employment * 4.0) + + >>> 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} - vectors = AequilibraeData() - vectors.create_empty(**args) + >>> args = {"entries": zones, "field_names": ["productions", "attractions"], + ... "data_types": [np.float64, np.float64], "memory_mode": True} + >>> vectors = AequilibraeData() + >>> vectors.create_empty(**args) # Assign the data to the vector object - vectors.productions[:] = df.production.values[:] - vectors.attractions[:] = df.attraction.values[:] - vectors.index[:] = zones[:] + >>> vectors.productions[:] = df.production.values[:] + >>> vectors.attractions[:] = df.attraction.values[:] + >>> vectors.index[:] = df.zone_id.values[:] # Balance the vectors - vectors.attractions[:] *= vectors.productions.sum() / vectors.attractions.sum() + >>> vectors.attractions[:] *= vectors.productions.sum() / vectors.attractions.sum() # Create the problem object - args = {"impedance": matrix, - "rows": vectors, - "row_field": "productions", - "model": model, - "columns": vectors, - "column_field": "attractions", - "output": 'path/to/output/matrix.aem', - "nan_as_zero":True - } - gravity = GravityApplication(**args) + >>> args = {"impedance": matrix, + ... "rows": vectors, + ... "row_field": "productions", + ... "model": model, + ... "columns": vectors, + ... "column_field": "attractions", + ... "output": '/tmp/test_project_ga/matrices/matrix.aem', + ... "nan_as_zero":True + ... } + >>> gravity = GravityApplication(**args) # Solve and save the outputs - gravity.apply() - gravity.output.export('path/to/omx_file.omx') - with open('path.to/report.txt', 'w') as f: - for line in gravity.report: - f.write(f'{line}\n') + >>> 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") + """ def __init__(self, project=None, **kwargs): """ Instantiates the Ipf problem - Args: - model (:obj:`SyntheticGravityModel`): Synthetic gravity model to apply + :Arguments: + **model** (:obj:`SyntheticGravityModel`): Synthetic gravity model to apply - impedance (:obj:`AequilibraeMatrix`): Impedance matrix to be used + **impedance** (:obj:`AequilibraeMatrix`): Impedance matrix to be used - rows (:obj:`AequilibraeData`): Vector object with data for row totals + **rows** (:obj:`AequilibraeData`): Vector object with data for row totals - row_field (:obj:`str`): Field name that contains the data for the row totals + **row_field** (:obj:`str`): Field name that contains the data for the row totals - columns (:obj:`AequilibraeData`): Vector object with data for column totals + **columns** (:obj:`AequilibraeData`): Vector object with data for column totals - column_field (:obj:`str`): Field name that contains the data for the column totals + **column_field** (:obj:`str`): Field name that contains the data for the column totals - project (:obj:`Project`, optional): The Project to connect to. By default, uses the currently active project + **project** (:obj:`Project`, optional): The Project to connect to. By default, uses the currently + active project - core_name (:obj:`str`, optional): Name for the output matrix core. Defaults to "gravity" + **core_name** (:obj:`str`, optional): Name for the output matrix core. Defaults to "gravity" - parameters (:obj:`str`, optional): Convergence parameters. Defaults to those in the parameter file + **parameters** (:obj:`str`, optional): Convergence parameters. Defaults to those in the parameter file - nan_as_zero (:obj:`bool`, optional): If Nan values should be treated as zero. Defaults to True + **nan_as_zero** (:obj:`bool`, optional): If Nan values should be treated as zero. Defaults to True - Results: - output (:obj:`AequilibraeMatrix`): Result Matrix + :Results: + **output** (:obj:`AequilibraeMatrix`): Result Matrix - report (:obj:`list`): Iteration and convergence report + **report** (:obj:`list`): Iteration and convergence report - error (:obj:`str`): Error description + **error** (:obj:`str`): Error description """ self.project = project @@ -224,10 +225,10 @@ def apply(self): def save_to_project(self, name: str, file_name: str, project=None) -> None: """Saves the matrix output to the project file - Args: - name (:obj:`str`): Name of the desired matrix record - file_name (:obj:`str`): Name for the matrix file name. AEM and OMX supported - project (:obj:`Project`, Optional): Project we want to save the results to. Defaults to the active project + :Arguments: + **name** (:obj:`str`): Name of the desired matrix record + **file_name** (:obj:`str`): Name for the matrix file name. AEM and OMX supported + **project** (:obj:`Project`, Optional): Project we want to save the results to. Defaults to the active project """ project = project or get_active_project() diff --git a/aequilibrae/distribution/gravity_calibration.py b/aequilibrae/distribution/gravity_calibration.py index 91c30e1f3..691c465d3 100644 --- a/aequilibrae/distribution/gravity_calibration.py +++ b/aequilibrae/distribution/gravity_calibration.py @@ -8,71 +8,75 @@ import numpy as np -from .gravity_application import GravityApplication, SyntheticGravityModel -from ..matrix import AequilibraeMatrix, AequilibraeData -from ..parameters import Parameters +from aequilibrae.distribution.gravity_application import GravityApplication, SyntheticGravityModel +from aequilibrae.matrix import AequilibraeMatrix, AequilibraeData +from aequilibrae.parameters import Parameters class GravityCalibration: - r""" - Calibrate a traditional gravity model + """Calibrate a traditional gravity model - Available deterrence function forms are: 'EXPO' or 'POWER'. 'GAMMA' - :: + Available deterrence function forms are: 'EXPO' or 'POWER'. 'GAMMA' - from aequilibrae.matrix import AequilibraeMatrix - from aequilibrae.distribution import GravityCalibration + .. 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") # We load the impedance matrix - matrix = AequilibraeMatrix() - matrix.load('path/to/trip_matrix.aem') - matrix.computational_view(['total_trips']) + >>> matrix = AequilibraeMatrix() + >>> matrix.load('/tmp/test_project_gc/matrices/demand.omx') + >>> matrix.computational_view(['matrix']) - # We load the impedance matrix - impedmatrix = AequilibraeMatrix() - impedmatrix.load('path/to/impedance_matrix.aem') - impedmatrix.computational_view(['traveltime']) + # We load the impedance matrix + >>> impedmatrix = AequilibraeMatrix() + >>> impedmatrix.load('/tmp/test_project_gc/matrices/skims.omx') + >>> impedmatrix.computational_view(['time_final']) # Creates the problem - args = {"matrix": matrix, - "impedance": impedmatrix, - "row_field": "productions", - "function": 'expo', - "nan_as_zero":True - } - gravity = GravityCalibration(**args) + >>> args = {"matrix": matrix, + ... "impedance": impedmatrix, + ... "row_field": "productions", + ... "function": 'expo', + ... "nan_as_zero": True} + >>> gravity = GravityCalibration(**args) # Solve and save outputs - gravity.calibrate() - gravity.model.save('path/to/dist_expo_model.mod') - with open('path.to/report.txt', 'w') as f: - for line in gravity.report: - f.write(f'{line}\n') + >>> 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') """ def __init__(self, project=None, **kwargs): """ Instantiates the Gravity calibration problem - Args: - matrix (:obj:`AequilibraeMatrix`): Seed/base trip matrix + :Arguments: + **matrix** (:obj:`AequilibraeMatrix`): Seed/base trip matrix - impedance (:obj:`AequilibraeMatrix`): Impedance matrix to be used + **impedance** (:obj:`AequilibraeMatrix`): Impedance matrix to be used - function (:obj:`str`): Function name to be calibrated. "EXPO" or "POWER" + **function** (:obj:`str`): Function name to be calibrated. "EXPO" or "POWER" - project (:obj:`Project`, optional): The Project to connect to. By default, uses the currently active project + **project** (:obj:`Project`, optional): The Project to connect to. By default, uses the currently active project - parameters (:obj:`str`, optional): Convergence parameters. Defaults to those in the parameter file + **parameters** (:obj:`str`, optional): Convergence parameters. Defaults to those in the parameter file - nan_as_zero (:obj:`bool`, optional): If Nan values should be treated as zero. Defaults to True + **nan_as_zero** (:obj:`bool`, optional): If Nan values should be treated as zero. Defaults to True - Results: - model (:obj:`SyntheticGravityModel`): Calibrated model + :Results: + **model** (:obj:`SyntheticGravityModel`): Calibrated model - report (:obj:`list`): Iteration and convergence report + **report** (:obj:`list`): Iteration and convergence report - error (:obj:`str`): Error description + **error** (:obj:`str`): Error description """ diff --git a/aequilibrae/distribution/ipf.py b/aequilibrae/distribution/ipf.py index 4df21c87c..2378c40a4 100644 --- a/aequilibrae/distribution/ipf.py +++ b/aequilibrae/distribution/ipf.py @@ -19,75 +19,70 @@ class Ipf: """Iterative proportional fitting procedure - :: + .. code-block:: python - import pandas as pd - from aequilibrae.distribution import Ipf - from aequilibrae.matrix import AequilibraeMatrix - from aequilibrae.matrix import AequilibraeData + >>> from aequilibrae import Project + >>> from aequilibrae.distribution import Ipf + >>> from aequilibrae.matrix import AequilibraeMatrix, AequilibraeData - matrix = AequilibraeMatrix() + >>> project = Project.from_path("/tmp/test_project_ipf") - # Here we can create from OMX or load from an AequilibraE matrix. - matrix.create_from_omx(path/to/aequilibrae_matrix, path/to/omxfile) - - # The matrix will be operated one (see the note on overwriting), so it does - # not make sense load an OMX matrix + >>> matrix = AequilibraeMatrix() + # Here we can create from OMX or load from an AequilibraE matrix. + >>> matrix.load('/tmp/test_project/matrices/demand.omx') + >>> matrix.computational_view() - source_vectors = pd.read_csv(path/to/CSVs) - zones = source_vectors.zone.shape[0] - - args = {"entries": zones, "field_names": ["productions", "attractions"], - "data_types": [np.float64, np.float64], "memory_mode": True} + >>> args = {"entries": matrix.zones, "field_names": ["productions", "attractions"], + ... "data_types": [np.float64, np.float64], "memory_mode": True} - vectors = AequilibraEData() - vectors.create_empty(**args) + >>> vectors = AequilibraeData() + >>> vectors.create_empty(**args) - vectors.productions[:] = source_vectors.productions[:] - vectors.attractions[:] = source_vectors.attractions[:] + >>> vectors.productions[:] = matrix.rows()[:] + >>> vectors.attractions[:] = matrix.columns()[:] # We assume that the indices would be sorted and that they would match the matrix indices - vectors.index[:] = source_vectors.zones[:] + >>> vectors.index[:] = matrix.index[:] - args = { - "matrix": matrix, "rows": vectors, "row_field": "productions", "columns": vectors, - "column_field": "attractions", "nan_as_zero": False} + >>> args = { + ... "matrix": matrix, "rows": vectors, "row_field": "productions", "columns": vectors, + ... "column_field": "attractions", "nan_as_zero": False} - fratar = Ipf(**args) + >>> fratar = Ipf(**args) - fratar.fit() + >>> fratar.fit() # We can get back to our OMX matrix in the end - fratar.output.export(path/to_omx/output.omx) - fratar.output.export(path/to_aem/output.aem) + >>> fratar.output.export("/tmp/to_omx_output.omx") + >>> fratar.output.export("/tmp/to_aem_output.aem") """ def __init__(self, project=None, **kwargs): """ Instantiates the Ipf problem - Args: - matrix (:obj:`AequilibraeMatrix`): Seed Matrix + :Arguments: + **matrix** (:obj:`AequilibraeMatrix`): Seed Matrix - rows (:obj:`AequilibraeData`): Vector object with data for row totals + **rows** (:obj:`AequilibraeData`): Vector object with data for row totals - row_field (:obj:`str`): Field name that contains the data for the row totals + **row_field** (:obj:`str`): Field name that contains the data for the row totals - columns (:obj:`AequilibraeData`): Vector object with data for column totals + **columns** (:obj:`AequilibraeData`): Vector object with data for column totals - column_field (:obj:`str`): Field name that contains the data for the column totals + **column_field** (:obj:`str`): Field name that contains the data for the column totals - parameters (:obj:`str`, optional): Convergence parameters. Defaults to those in the parameter file + **parameters** (:obj:`str`, optional): Convergence parameters. Defaults to those in the parameter file - nan_as_zero (:obj:`bool`, optional): If Nan values should be treated as zero. Defaults to True + **nan_as_zero** (:obj:`bool`, optional): If Nan values should be treated as zero. Defaults to True - Results: - output (:obj:`AequilibraeMatrix`): Result Matrix + :Results: + **output** (:obj:`AequilibraeMatrix`): Result Matrix - report (:obj:`list`): Iteration and convergence report + **report** (:obj:`list`): Iteration and convergence report - error (:obj:`str`): Error description + **error** (:obj:`str`): Error description """ self.cpus = 0 self.parameters = kwargs.get("parameters", self.__get_parameters("ipf")) @@ -234,10 +229,11 @@ def fit(self): def save_to_project(self, name: str, file_name: str, project=None) -> MatrixRecord: """Saves the matrix output to the project file - Args: - name (:obj:`str`): Name of the desired matrix record - file_name (:obj:`str`): Name for the matrix file name. AEM and OMX supported - project (:obj:`Project`, Optional): Project we want to save the results to. Defaults to the active project + :Arguments: + **name** (:obj:`str`): Name of the desired matrix record + **file_name** (:obj:`str`): Name for the matrix file name. AEM and OMX supported + **project** (:obj:`Project`, Optional): Project we want to save the results to. + Defaults to the active project """ project = project or get_active_project() diff --git a/aequilibrae/log.py b/aequilibrae/log.py index 3d90946ac..c07e67a89 100644 --- a/aequilibrae/log.py +++ b/aequilibrae/log.py @@ -2,26 +2,25 @@ import tempfile import logging -from .parameters import Parameters +from aequilibrae.parameters import Parameters class Log: """API entry point to the log file contents - :: + .. code-block:: python - from aequilibrae import Project + >>> from aequilibrae import Project - p = Project() - p.open('path/to/project/folder') + >>> project = Project.from_path("/tmp/test_project") - log = p.log() + >>> log = project.log() # We get all entries for the log file - entries = log.contents() + >>> entries = log.contents() # Or clear everything (NO UN-DOs) - log.clear() + >>> log.clear() """ def __init__(self, project_base_path: str): @@ -30,8 +29,8 @@ def __init__(self, project_base_path: str): def contents(self) -> list: """Returns contents of log file - Return: - *log_contents* (:obj:`list`): List with all entries in the log file + :Return: + **log_contents** (:obj:`list`): List with all entries in the log file """ with open(self.log_file_path, "r") as file: @@ -60,7 +59,7 @@ def _setup_logger(): def get_log_handler(log_file: str, ensure_file_exists=True): - """return a log handler that writes to the given log_file""" + """Return a log handler that writes to the given log_file""" if os.path.exists(log_file) and not os.path.isfile(log_file): raise FileExistsError(f"{log_file} is not a valid file") diff --git a/aequilibrae/matrix/aequilibrae_data.py b/aequilibrae/matrix/aequilibrae_data.py index 6332a5309..ee8074027 100644 --- a/aequilibrae/matrix/aequilibrae_data.py +++ b/aequilibrae/matrix/aequilibrae_data.py @@ -36,32 +36,40 @@ def create_empty( """ Creates a new empty dataset - Args: - *file_path* (:obj:`str`, Optional): Full path for the output data file. If *memory_false* is 'false' and + :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 - *entries* (:obj:`int`, Optional): Number of records in the dataset. Default is 1 + **entries** (:obj:`int`, Optional): Number of records in the dataset. Default is 1 - *field_names* (:obj:`list`, Optional): List of field names for this dataset. If no list is provided, the + **field_names** (:obj:`list`, Optional): List of field names for this dataset. If no list is provided, the field 'data' will be created - *data_types* (:obj:`np.dtype`, Optional): List of data types for the dataset. Types need to be NumPy data + **data_types** (:obj:`np.dtype`, Optional): List of data types for the dataset. Types need to be NumPy data types (e.g. np.int16, np.float64). If no list of types are provided, type will be *np.float64* - *memory_mode* (:obj:`bool`, Optional): If true, dataset will be kept in memory. If false, the dataset will + **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 - vectors = "D:/release/Sample models/Chicago_2020_02_15/vectors.aed" - args = { - "file_path": vectors, - "entries": vec_1.shape[0], - "field_names": ["origins", "destinations"], - "data_types": [np.float64, np.float64], - } - dataset = AequilibraeData() - dataset.create_empty(**args) + >>> 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) """ @@ -135,13 +143,15 @@ def load(self, file_path): """ Loads dataset from file - Args: - *file_path* (:obj:`str`): Full file path to the AequilibraeData to be loaded + :Arguments: + **file_path** (:obj:`str`): Full file path to the AequilibraeData to be loaded - :: + .. code-block:: python - dataset = AequilibraeData() - dataset.load("D:/datasets/vectors.aed") + >>> 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) @@ -159,16 +169,18 @@ def export(self, file_name, table_name="aequilibrae_table"): """ Exports the dataset to another format. Supports CSV and SQLite - Args: - *file_name* (:obj:`str`): File name with PATH and extension (csv, or sqlite3, sqlite or db) + :Arguments: + **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 - *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("D:/datasets/vectors.aed") - dataset.export("D:/datasets/vectors.csv") + >>> dataset = AequilibraeData() + >>> dataset.load("/tmp/test_project/vectors.aed") + >>> dataset.export("/tmp/test_project/vectors.csv") """ file_type = os.path.splitext(file_name)[1] @@ -221,9 +233,13 @@ def random_name(): """ Returns a random name for a dataset with root in the temp directory of the user - :: + .. code-block:: python + + >>> from aequilibrae.matrix import AequilibraeData + + >>> name = AequilibraeData().random_name() # doctest: +ELLIPSIS - name = AequilibraeData().random_name() - '/tmp/Aequilibrae_data_5werr5f36-b123-asdf-4587-adfglkjhqwe.aed' + # This is an example of output + # '/tmp/Aequilibrae_data_5werr5f36-b123-asdf-4587-adfglkjhqwe.aed' """ 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 b5bd1ff61..11928d9a1 100644 --- a/aequilibrae/matrix/aequilibrae_matrix.py +++ b/aequilibrae/matrix/aequilibrae_matrix.py @@ -92,10 +92,11 @@ def __init__(self): def save(self, names=()) -> None: """Saves matrix data back to file. - If working with AEM file, it flushes data to disk. If working with OMX, requires new names + If working with AEM file, it flushes data to disk. If working with OMX, requires new names. - Args: - *names* (:obj:`tuple(str)`, `Optional`): New names for the matrices. Required if working with OMX files""" + :Arguments: + **names** (:obj:`tuple(str)`, `Optional`): New names for the matrices. Required if working with OMX files + """ if not self.__omx: self.__flush(self.matrices) @@ -133,41 +134,42 @@ def create_empty( """ Creates an empty matrix in the AequilibraE format - Args: - *file_name* (:obj:`str`): Local path to the matrix file + :Arguments: + **file_name** (:obj:`str`): Local path to the matrix file - *zones* (:obj:`int`): Number of zones in the model (Integer). Maximum number of zones in a matrix is + **zones** (:obj:`int`): Number of zones in the model (Integer). Maximum number of zones in a matrix is 4,294,967,296 - *matrix_names* (:obj:`list`): A regular Python list of names of the matrix. Limit is 50 characters each. + **matrix_names** (:obj:`list`): A regular Python list of names of the matrix. Limit is 50 characters each. Maximum number of cores per matrix is 256 - *file_name* (:obj:`str`): Local path to the matrix file - - *data_type* (:obj:`np.dtype`, optional): Data type of the matrix as NUMPY data types (NP.int32, np.int64, + **data_type** (:obj:`np.dtype`, optional): Data type of the matrix as NUMPY data types (NP.int32, np.int64, np.float32, np.float64). Defaults to np.float64 - *index_names* (:obj:`list`, optional): A regular Python list of names for indices. Limit is 20 characters + **index_names** (:obj:`list`, optional): A regular Python list of names for indices. Limit is 20 characters each). Maximum number of indices per matrix is 256 - *compressed* (:obj:`bool`, optional): Whether it is a flat matrix or a compressed one (Boolean - Not yet + **compressed** (:obj:`bool`, optional): Whether it is a flat matrix or a compressed one (Boolean - Not yet implemented) - :: - - zones_in_the_model = 3317 - names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] - - mat = AequilibraeMatrix() - mat.create_empty(file_name='my/path/to/file', - zones=zones_in_the_model, - matrix_names= names_list) - mat.num_indices - 1 - mat.zones - 3317 - np.sum(mat[trips]) - 0.0 + **memory_only** (:obj:`bool`, optional): Whether you want to keep the matrix copy in memory only. Defaults to True + + .. code-block:: python + + >>> from aequilibrae.matrix import AequilibraeMatrix + + >>> zones_in_the_model = 3317 + >>> 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, + ... memory_only=False,) + >>> mat.num_indices + 1 + >>> mat.zones + 3317 """ self.__memory_only = memory_only @@ -227,13 +229,12 @@ def create_empty( self.__write__() def get_matrix(self, core: str, copy=False) -> np.ndarray: - """ - Returns the data for a matrix core + """Returns the data for a matrix core - Args: - *core* (:obj:`str`): name of the matrix core to be returned + :Arguments: + **core** (:obj:`str`): name of the matrix core to be returned - *copy* (:obj:`bool`, optional): return a copy of the data. Defaults to False + **copy** (:obj:`bool`, optional): return a copy of the data. Defaults to False :Returns: @@ -262,22 +263,22 @@ def create_from_omx( """ Creates an AequilibraeMatrix from an original OpenMatrix - Args: - *file_path* (:obj:`str`): Path for the output AequilibraEMatrix + :Arguments: + **file_path** (:obj:`str`): Path for the output AequilibraEMatrix - *omx_path* (:obj:`str`): Path to the OMX file one wants to import + **omx_path** (:obj:`str`): Path to the OMX file one wants to import - *cores* (:obj:`list`): List of matrix cores to be imported + **cores** (:obj:`list`): List of matrix cores to be imported - *mappings* (:obj:`list`): List of the matrix mappings (i.e. indices, centroid numbers) to be imported + **mappings** (:obj:`list`): List of the matrix mappings (i.e. indices, centroid numbers) to be imported - *robust* (:obj:`bool`, optional): Boolean for whether AequilibraE should try to adjust the names for cores + **robust** (:obj:`bool`, optional): Boolean for whether AequilibraE should try to adjust the names for cores and indices in case they are too long. Defaults to True - *compressed* (:obj:`bool`, optional): Boolean for whether we should compress the output matrix. + **compressed** (:obj:`bool`, optional): Boolean for whether we should compress the output matrix. Not yet implemented - *memory_only* (:obj:`bool`, optional): Whether you want to keep the matrix copy in memory only. Defaults to False + **memory_only** (:obj:`bool`, optional): Whether you want to keep the matrix copy in memory only. Defaults to True """ @@ -374,14 +375,14 @@ def create_from_trip_list(self, path_to_file: str, from_column: str, to_column: Creates an AequilibraeMatrix from a trip list csv file The output is saved in the same folder as the trip list file - Args: - *path_to_file* (:obj:`str`): Path for the trip list csv file + :Arguments: + **path_to_file** (:obj:`str`): Path for the trip list csv file - *from_column* (:obj:`str`): trip list file column containing the origin zones numbers + **from_column** (:obj:`str`): trip list file column containing the origin zones numbers - *from_column* (:obj:`str`): trip list file column containing the destination zones numbers + **from_column** (:obj:`str`): trip list file column containing the destination zones numbers - *list_cores* (:obj:`list`): list of core columns in the trip list file + **list_cores** (:obj:`list`): list of core columns in the trip list file """ @@ -634,27 +635,29 @@ def set_index(self, index_to_set: str) -> None: Sets the standard index to be the one the user wants to have be the one being used in all operations during run time. The first index is ALWAYS the default one every time the matrix is instantiated - Args: - index_to_set (:obj:`str`): Name of the index to be used. The default index name is 'main_index' - - :: - - zones_in_the_model = 3317 - names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] - index_list = ['tazs', 'census'] - - mat = AequilibraeMatrix() - mat.create_empty(file_name='my/path/to/file', - zones=zones_in_the_model, - matrix_names=names_list, - index_names =index_list ) - mat.num_indices - 2 - mat.current_index - 'tazs' - mat.set_index('census') - mat.current_index - 'census' + :Arguments: + **index_to_set** (:obj:`str`): Name of the index to be used. The default index name is 'main_index' + + .. code-block:: python + + >>> from aequilibrae.matrix import AequilibraeMatrix + + >>> zones_in_the_model = 3317 + >>> names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] + >>> index_list = ['tazs', 'census'] + + >>> mat = AequilibraeMatrix() + >>> mat.create_empty(file_name="/tmp/path_to_new_matrix.aem", + ... zones=zones_in_the_model, + ... matrix_names=names_list, + ... index_names=index_list ) + >>> mat.num_indices + 2 + >>> mat.current_index + 'tazs' + >>> mat.set_index('census') + >>> mat.current_index + 'census' """ if self.__omx: self.index = np.array(list(self.omx_file.mapping(index_to_set).keys())) @@ -713,34 +716,37 @@ def close(self): def export(self, output_name: str, cores: List[str] = None): """ - Exports the matrix to other formats. Formats currently supported: CSV, OMX + Exports the matrix to other formats, rather than AEM. Formats currently supported: CSV, OMX When exporting to AEM or OMX, the user can chose to export only a set of cores, but all indices are exported When exporting to CSV, the active index will be used, and all cores will be exported as separate columns in the output file - Args: - *output_name* (:obj:`str`): Path to the output file + :Arguments: + **output_name** (:obj:`str`): Path to the output file - *cores* (:obj:`list`): Names of the cores to be exported. + **cores** (:obj:`list`): Names of the cores to be exported. - :: + .. code-block:: python - zones_in_the_model = 3317 - names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] + >>> from aequilibrae.matrix import AequilibraeMatrix - mat = AequilibraeMatrix() - mat.create_empty(file_name='my/path/to/file', zones=zones_in_the_model, matrix_names= names_list) - mat.cores - ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] + >>> zones_in_the_model = 3317 + >>> names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] - mat.export('my_new_path', ['Car trips', 'bike trips']) + >>> mat = AequilibraeMatrix() + >>> mat.create_empty(file_name='/tmp/path_to_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']) - mat2 = AequilibraeMatrix() - mat2.load('my_new_path') - mat2.cores - ['Car trips', 'bike trips'] + >>> mat2 = AequilibraeMatrix() + >>> mat2.load('/tmp/my_new_path.aem') + >>> mat2.cores + 2 """ if self.__omx: @@ -783,24 +789,20 @@ def f(name): def load(self, file_path: str): """ - Loads matrix from disk. All cores and indices are load. First index is default - - Args: - file_path (:obj:`str`): Path to AEM or OMX file on disk + Loads matrix from disk. All cores and indices are load. First index is default. - :: + :Arguments: + **file_path** (:obj:`str`): Path to AEM or OMX file on disk - zones_in_the_model = 3317 - names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] + .. code-block:: python - mat = AequilibraeMatrix() - mat.create_empty(file_name='my/path/to/file', zones=zones_in_the_model, matrix_names= names_list) - mat.close() + >>> from aequilibrae.matrix import AequilibraeMatrix - mat2 = AequilibraeMatrix() - mat2.load('my/path/to/file.omx') - mat2.zones - 3317 + >>> mat = AequilibraeMatrix() + >>> mat.load('/tmp/path_to_matrix.aem') + >>> mat.computational_view(["bike trips"]) + >>> mat.names + ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] """ self.file_path = file_path @@ -824,19 +826,23 @@ def computational_view(self, core_list: List[str] = None): In case of OMX matrices, the computational view is held only in memory - Args: - *core_list* (:obj:`list`): List with the names of all matrices that need to be in the buffer + :Arguments: + **core_list** (:obj:`list`): List with the names of all matrices that need to be in the buffer - :: + .. code-block:: python - zones_in_the_model = 3317 - names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] + >>> from aequilibrae.matrix import AequilibraeMatrix - mat = AequilibraeMatrix() - mat.create_empty(file_name='my/path/to/file', zones=zones_in_the_model, matrix_names= names_list) - mat.computational_view(['bike trips', 'walk trips']) - mat.view_names - ['bike trips', 'walk trips'] + >>> zones_in_the_model = 3317 + >>> 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.computational_view(['bike trips', 'walk trips']) + >>> mat.view_names + ['bike trips', 'walk trips'] """ self.matrix_view = None @@ -885,33 +891,39 @@ def copy( """ Copies a list of cores (or all cores) from one matrix file to another one - Args: - *output_name* (:obj:`str`): Name of the new matrix file. If none is provided, returns a copy in memory only + :Arguments: + **output_name** (:obj:`str`): Name of the new matrix file. If none is provided, returns a copy in memory only - *cores* (:obj:`list`):List of the matrix cores to be copied + **cores** (:obj:`list`):List of the matrix cores to be copied - *names* (:obj:`list`, optional): List with the new names for the cores. Defaults to current names + **names** (:obj:`list`, optional): List with the new names for the cores. Defaults to current names - *compress* (:obj:`bool`, optional): Whether you want to compress the matrix or not. Defaults to False + **compress** (:obj:`bool`, optional): Whether you want to compress the matrix or not. Defaults to False Not yet implemented - *memory_only* (:obj:`bool`, optional): Whether you want to keep the matrix copy in memory only. Defaults to False + **memory_only** (:obj:`bool`, optional): Whether you want to keep the matrix copy in memory only. + Defaults to True - :: + .. code-block:: python - zones_in_the_model = 3317 - names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] + >>> from aequilibrae.matrix import AequilibraeMatrix - mat = AequilibraeMatrix() - mat.create_empty(file_name='my/path/to/file', zones=zones_in_the_model, matrix_names= names_list) - mat.copy('my/new/path/to/file', cores=['bike trips', 'walk trips'], names=['bicycle', 'walking']) + >>> zones_in_the_model = 3317 + >>> names_list = ['Car trips', 'pt trips', 'DRT trips', 'bike trips', 'walk trips'] - mat2 = AequilibraeMatrix() - mat2.load('my/new/path/to/file') - mat.cores - ['bicycle', 'walking'] - """ + >>> 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', + ... cores=['bike trips', 'walk trips'], + ... names=['bicycle', 'walking'], + ... memory_only=False) # doctest: +ELLIPSIS + + >>> mat2 = AequilibraeMatrix() + >>> mat2.load('/tmp/path_to_copy.aem') + >>> mat2.cores + 2 + """ if compress: raise Warning("Matrix compression not yet supported") @@ -958,15 +970,22 @@ def rows(self) -> np.ndarray: :Returns: - *object* (:obj:`np.ndarray`): the row totals for the matrix currently on the computational view + **object** (:obj:`np.ndarray`): the row totals for the matrix currently on the computational view + + .. code-block:: python - :: + >>> from aequilibrae.matrix import AequilibraeMatrix - mat = AequilibraeMatrix() - mat.load('my/path/to/file') - mat.computational_view(mat.cores[0]) - mat.rows() - array([0.,...,0.]) + >>> mat = AequilibraeMatrix() + >>> mat.load('/tmp/test_project/matrices/skims.omx') + >>> mat.computational_view(["distance_blended"]) + >>> mat.rows() + array([357.68202084, 358.68778868, 310.68285491, 275.87964738, + 265.91709918, 268.60184371, 267.32264726, 281.3793747 , + 286.15085073, 242.60308705, 252.1776242 , 305.56774194, + 303.58100777, 270.48841269, 263.20417379, 253.92665702, + 277.1655432 , 258.84368258, 280.65697316, 272.7651157 , + 264.06806038, 252.87533845, 273.45639965, 281.61102767]) """ return self.__vector(axis=0) @@ -980,13 +999,20 @@ def columns(self) -> np.ndarray: *object* (:obj:`np.ndarray`): the column totals for the matrix currently on the computational view - :: + .. code-block:: python + + >>> from aequilibrae.matrix import AequilibraeMatrix - mat = AequilibraeMatrix() - mat.load('my/path/to/file') - mat.computational_view(mat.cores[0]) - mat.columns() - array([0.34,.0.,...,14.03]) + >>> mat = AequilibraeMatrix() + >>> mat.load('/tmp/test_project/matrices/skims.omx') + >>> mat.computational_view(["distance_blended"]) + >>> mat.columns() + array([357.54256811, 357.45109051, 310.88655449, 276.6783439 , + 266.70388637, 270.62976319, 266.32888632, 279.6897402 , + 285.89821842, 242.79743295, 252.34085912, 301.78116548, + 302.97058146, 270.61855294, 264.59944248, 257.83842251, + 276.63310578, 257.74513863, 281.15724257, 271.63886077, + 264.62215032, 252.79791125, 273.18139747, 282.7636574 ]) """ return self.__vector(axis=1) @@ -994,12 +1020,14 @@ def nan_to_num(self): """ Converts all NaN values in all cores in the computational view to zeros - :: + .. code-block:: python - mat = AequilibraeMatrix() - mat.load('my/path/to/file') - mat.computational_view(mat.cores[0]) - mat.nan_to_num() + >>> from aequilibrae.matrix import AequilibraeMatrix + + >>> mat = AequilibraeMatrix() + >>> mat.load('/tmp/path_to_matrix.aem') + >>> mat.computational_view(["bike trips"]) + >>> mat.nan_to_num() """ if self.__omx: @@ -1037,16 +1065,18 @@ def setName(self, matrix_name: str): """ Sets the name for the matrix itself - Args: - *matrix_name* (:obj:`str`): matrix name. Maximum length is 50 characters + :Arguments: + **matrix_name** (:obj:`str`): matrix name. Maximum length is 50 characters + + .. code-block:: python - :: + >>> from aequilibrae.matrix import AequilibraeMatrix - mat = AequilibraeMatrix() - mat.load('my/path/to/file') - mat.setName('This is my example') - mat.name - 'This is my example' + >>> mat = AequilibraeMatrix() + >>> mat.create_empty(file_name="matrix.aem", zones=3317, memory_only=False) + >>> mat.setName('This is my example') + >>> mat.name + '' """ if self.__omx: raise NotImplementedError("This operation does not make sense for OMX matrices") @@ -1063,16 +1093,23 @@ def setDescription(self, matrix_description: str): """ Sets description for the matrix - Args: - *matrix_description* (:obj:`str`): Text with matrix description . Maximum length is 144 characters + :Arguments: + **matrix_description** (:obj:`str`): Text with matrix description . Maximum length is 144 characters - :: + .. code-block:: python - mat = AequilibraeMatrix() - mat.load('my/path/to/file') - mat.setDescription('This is some text about this matrix of mine') - mat.description - 'This is some text about this matrix of mine' + >>> from aequilibrae.matrix import AequilibraeMatrix + + >>> 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.save() + >>> mat.close() + + >>> mat = AequilibraeMatrix() + >>> mat.load("matrix.aem") + >>> mat.description.decode('utf-8') + 'This is some text ab' """ if self.__omx: raise NotImplementedError("This operation does not make sense for OMX matrices") @@ -1094,9 +1131,13 @@ def random_name() -> str: """ Returns a random name for a matrix with root in the temp directory of the user - :: + .. code-block:: python + + >>> from aequilibrae.matrix import AequilibraeMatrix + + >>> name = AequilibraeMatrix().random_name() # doctest: +ELLIPSIS - name = AequilibraeMatrix().random_name() - '/tmp/Aequilibrae_matrix_54625f36-bf41-4c85-80fb-7fc2e3f3d76e.aem' + # This is an example of output + # '/tmp/Aequilibrae_matrix_54625f36-bf41-4c85-80fb-7fc2e3f3d76e.aem' """ return os.path.join(tempfile.gettempdir(), f"Aequilibrae_matrix_{uuid.uuid4()}.aem") diff --git a/aequilibrae/parameters.py b/aequilibrae/parameters.py index 23721e9b3..e5c5c7f1d 100644 --- a/aequilibrae/parameters.py +++ b/aequilibrae/parameters.py @@ -6,31 +6,33 @@ class Parameters: - """ - Global parameters module + """Global parameters module Parameters are used in many procedures, and are often defined only in the parameters.yml file ONLY Parameters are organized in the following groups: * assignment * distribution - * system - - cpus: Maximum threads to be used in any procedure + * report zeros + * temp directory - - default_directory: If is the directory QGIS file opening/saving dialogs will try to open as standard + .. code-block:: python - - driving side: For purposes of plotting on QGIS + >>> from aequilibrae import Project, Parameters - - logging: Level of logging to be written to temp/aequilibrae.log: Levels are those from the Python logging library - - 0: 'NOTSET' - - 10: 'DEBUG' - - 20: 'INFO' - - 30: 'WARNING' - - 40: 'ERROR' - - 50: 'CRITICAL' - * report zeros - * temp directory + >>> project = Project.from_path("/tmp/test_project") + + >>> p = Parameters(project) + + >>> p.parameters['system']['logging_directory'] = "/tmp/other_folder" + >>> 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 + >>> p.write_back() + + >>> # You can also restore the software default values + >>> p.restore_default() """ _default: dict diff --git a/aequilibrae/paths/assignment_paths.py b/aequilibrae/paths/assignment_paths.py index e425eab11..a1a1489cb 100644 --- a/aequilibrae/paths/assignment_paths.py +++ b/aequilibrae/paths/assignment_paths.py @@ -58,7 +58,9 @@ 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) """ @@ -66,10 +68,12 @@ class AssignmentPaths(object): def __init__(self, table_name: str, project=None) -> None: """ Instantiates the class - Args: - table_name (str): Name of the traffic assignment result table used to generate the required path files - project (:obj:`Project`, optional): The Project to connect to. By default, uses the currently active project + :Arguments: + **table_name** (str): Name of the traffic assignment result table used to generate the required path files + + **project** (:obj:`Project`, optional): The Project to connect to. + By default, uses the currently active project """ project = project or get_active_project() diff --git a/aequilibrae/paths/graph.py b/aequilibrae/paths/graph.py index ed35fb02e..3ece684ec 100644 --- a/aequilibrae/paths/graph.py +++ b/aequilibrae/paths/graph.py @@ -79,8 +79,8 @@ def default_types(self, tp: str): """ Returns the default integer and float types used for computation - Args: - tp (:obj:`str`): data type. 'int' or 'float' + :Arguments: + **tp** (:obj:`str`): data type. 'int' or 'float' """ if tp == "int": return self.__integer_type @@ -100,8 +100,8 @@ def prepare_graph(self, centroids: np.ndarray) -> None: the inference that all links connected to these nodes are centroid connectors. - Args: - centroids (:obj:`np.ndarray`): Array with centroid IDs. Mandatory type Int64, unique and positive + :Arguments: + **centroids** (:obj:`np.ndarray`): Array with centroid IDs. Mandatory type Int64, unique and positive """ self.__network_error_checking__() @@ -221,8 +221,8 @@ 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 - Args: - links (:obj:`list`): List of link IDs to be excluded from the graph + :Arguments: + **links** (:obj:`list`): List of link IDs to be excluded from the graph """ filter = self.network.link_id.isin(links) # We check is the list makes sense in order to warn the user @@ -281,8 +281,8 @@ def set_graph(self, cost_field) -> None: """ Sets the field to be used for path computation - Args: - cost_field (:obj:`str`): Field name. Must be numeric + :Arguments: + **cost_field** (:obj:`str`): Field name. Must be numeric """ if cost_field in self.graph.columns: self.cost_field = cost_field @@ -303,8 +303,8 @@ def set_skimming(self, skim_fields: list) -> None: """ Sets the list of skims to be computed - Args: - skim_fields (:obj:`list`): Fields must be numeric + :Arguments: + **skim_fields** (:obj:`list`): Fields must be numeric """ if not skim_fields: self.skim_fields = [] @@ -342,8 +342,8 @@ def set_blocked_centroid_flows(self, block_centroid_flows) -> None: Default value is True - Args: - block_centroid_flows (:obj:`bool`): Blocking or not + :Arguments: + **block_centroid_flows** (:obj:`bool`): Blocking or not """ if not isinstance(block_centroid_flows, bool): raise TypeError("Blocking flows through centroids needs to be boolean") @@ -357,8 +357,8 @@ def save_to_disk(self, filename: str) -> None: """ Saves graph to disk - Args: - filename (:obj:`str`): Path to file. Usual file extension is *aeg* + :Arguments: + **filename** (:obj:`str`): Path to file. Usual file extension is *aeg* """ mygraph = {} mygraph["description"] = self.description @@ -386,8 +386,8 @@ def load_from_disk(self, filename: str) -> None: """ Loads graph from disk - Args: - filename (:obj:`str`): Path to file + :Arguments: + **filename** (:obj:`str`): Path to file """ with open(filename, "rb") as f: mygraph = pickle.load(f) @@ -419,8 +419,8 @@ def available_skims(self) -> List[str]: """ Returns graph fields that are available to be set as skims - Returns: - *list* (:obj:`str`): Field names + :Returns: + **list** (:obj:`str`): Field names """ return [x for x in self.graph.columns if x not in ["link_id", "a_node", "b_node", "direction", "id"]] diff --git a/aequilibrae/paths/network_skimming.py b/aequilibrae/paths/network_skimming.py index ded50a86e..4d9b6f9e0 100644 --- a/aequilibrae/paths/network_skimming.py +++ b/aequilibrae/paths/network_skimming.py @@ -29,36 +29,37 @@ class NetworkSkimming(WorkerThread): """ - :: + .. code-block:: python - from aequilibrae.paths.network_skimming import NetworkSkimming - from aequilibrae.project import Project + >>> from aequilibrae import Project + >>> from aequilibrae.paths.network_skimming import NetworkSkimming - project = Project() - project.open(self.proj_dir) - network = self.project.network + >>> project = Project.from_path("/tmp/test_project") - network.build_graphs() - graph = network.graphs['c'] - graph.set_graph(cost_field="distance") - graph.set_skimming("distance") + >>> network = project.network + >>> network.build_graphs() - skm = NetworkSkimming(graph) - skm.execute() + >>> graph = network.graphs['c'] + >>> graph.set_graph(cost_field="distance") + >>> graph.set_skimming("distance") + + >>> skm = NetworkSkimming(graph) + >>> skm.execute() # The skim report (if any error generated) is available here - skm.report + >>> skm.report + [] # To access the skim matrix directly from its temporary file - matrix = skm.results.skims + >>> matrix = skm.results.skims # Or you can save the results to disk - skm.save_to_project('skimming result') + >>> skm.save_to_project('/tmp/skimming result.omx') # Or specify the AequilibraE's matrix file format - skm.save_to_project('skimming result', 'aem') + >>> skm.save_to_project('skimming result', 'aem') - project.close() + >>> project.close() """ if pyqt: @@ -111,10 +112,10 @@ def execute(self): def save_to_project(self, name: str, format="omx", project=None) -> None: """Saves skim results to the project folder and creates record in the database - Args: - *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' - project (:obj:`Project`, Optional): Project we want to save the results to. Defaults to the active project + :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' + **project** (:obj:`Project`, `Optional`): Project we want to save the results to. Defaults to the active project """ file_name = f"{name}.{format.lower()}" diff --git a/aequilibrae/paths/results/assignment_results.py b/aequilibrae/paths/results/assignment_results.py index ff1335810..7d62241d0 100644 --- a/aequilibrae/paths/results/assignment_results.py +++ b/aequilibrae/paths/results/assignment_results.py @@ -76,10 +76,10 @@ def prepare(self, graph: Graph, matrix: AequilibraeMatrix) -> None: """ Prepares the object with dimensions corresponding to the assignment matrix and graph objects - Args: - *graph* (:obj:`Graph`): Needs to have been set with number of centroids and list of skims (if any) + :Arguments: + **graph** (:obj:`Graph`): Needs to have been set with number of centroids and list of skims (if any) - *matrix* (:obj:`AequilibraeMatrix`): Matrix properly set for computation with + **matrix** (:obj:`AequilibraeMatrix`): Matrix properly set for computation with matrix.computational_view(:obj:`list`) """ @@ -210,8 +210,8 @@ def set_cores(self, cores: int) -> None: Resulting number of cores will be adjusted to a minimum of zero or the maximum available in the system if the inputs result in values outside those limits - Args: - *cores* (:obj:`int`): Number of cores to be used in computation + :Arguments: + **cores** (:obj:`int`): Number of cores to be used in computation """ if not isinstance(cores, int): @@ -243,8 +243,8 @@ def get_load_results(self) -> AequilibraeData: """ Translates the assignment results from the graph format into the network format - Returns: - dataset (:obj:`AequilibraeData`): AequilibraE data with the traffic class assignment results + :Returns: + **dataset** (:obj:`AequilibraeData`): AequilibraE data with the traffic class assignment results """ fields = [e for n in self.classes["names"] for e in [f"{n}_ab", f"{n}_ba", f"{n}_tot"]] types = [np.float64] * len(fields) @@ -311,12 +311,14 @@ def get_sl_results(self) -> AequilibraeData: return res def save_to_disk(self, file_name=None, output="loads") -> None: - """DEPRECATED - Function to write to disk all outputs computed during assignment + """ + Function to write to disk all outputs computed during assignment. + + .. deprecated:: 0.7.0 - Args: - *file_name* (:obj:`str`): Name of the file, with extension. Valid extensions are: ['aed', 'csv', 'sqlite'] - *output* (:obj:`str`, optional): Type of output ('loads', 'path_file'). Defaults to 'loads' + :Arguments: + **file_name** (:obj:`str`): Name of the file, with extension. Valid extensions are: ['aed', 'csv', 'sqlite'] + **output** (:obj:`str`, optional): Type of output ('loads', 'path_file'). Defaults to 'loads' """ if output == "loads": diff --git a/aequilibrae/paths/results/path_results.py b/aequilibrae/paths/results/path_results.py index 2e38b277c..7748549de 100644 --- a/aequilibrae/paths/results/path_results.py +++ b/aequilibrae/paths/results/path_results.py @@ -10,30 +10,29 @@ class PathResults: - """ - Path computation result holder + """Path computation result holder + + .. code-block:: python - :: + >>> from aequilibrae import Project + >>> from aequilibrae.paths.results import PathResults - from aequilibrae.project import Project - from aequilibrae.paths.results import PathResults + >>> proj = Project.from_path("/tmp/test_project") + >>> proj.network.build_graphs() - proj = Project() - proj.load('path/to/project/folder') - proj.network.build_graphs() # Mode c is car in this project - car_graph = proj.network.graphs['c'] + >>> car_graph = proj.network.graphs['c'] # minimize distance - car_graph.set_graph('distance') + >>> car_graph.set_graph('distance') # If you want to compute skims # It does increase path computation time substantially - car_graph.set_skimming(['distance', 'travel_time']) + >>> car_graph.set_skimming(['distance', 'free_flow_time']) - res = PathResults() - res.prepare(car_graph) - res.compute_path(17, 13199) + >>> res = 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 @@ -41,11 +40,11 @@ class PathResults: # 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 1265. Same origin: 17 - res.update_trace(1265) + # Update all the outputs mentioned above for destination 9. Same origin: 1 + >>> res.update_trace(9) # clears all computation results - res.reset() + >>> res.reset() """ def __init__(self) -> None: @@ -74,10 +73,10 @@ def compute_path(self, origin: int, destination: int) -> None: """ Computes the path between two nodes in the network - Args: - *origin* (:obj:`int`): Origin for the path + :Arguments: + **origin** (:obj:`int`): Origin for the path - *destination* (:obj:`int`): Destination for the path + **destination** (:obj:`int`): Destination for the path """ if self.graph is None: @@ -93,8 +92,8 @@ def prepare(self, graph: Graph) -> None: """ Prepares the object with dimensions corresponding to the graph object - Args: - *graph* (:obj:`Graph`): Needs to have been set with number of centroids and list of skims (if any) + :Arguments: + **graph** (:obj:`Graph`): Needs to have been set with number of centroids and list of skims (if any) """ if not graph.cost_field: @@ -147,8 +146,8 @@ def update_trace(self, destination: int) -> None: It does not re-compute the path tree, so it saves most of the computation time - Args: - *destination* (:obj:`int`): ID of the node we are computing the path too + :Arguments: + **destination** (:obj:`int`): ID of the node we are computing the path too """ if not isinstance(destination, int): raise TypeError("destination needs to be an integer") diff --git a/aequilibrae/paths/results/skim_results.py b/aequilibrae/paths/results/skim_results.py index 74e6fa75d..e522175ae 100644 --- a/aequilibrae/paths/results/skim_results.py +++ b/aequilibrae/paths/results/skim_results.py @@ -6,30 +6,29 @@ class SkimResults: """ - Network skimming result holder + Network skimming result holder. - :: + .. code-block:: python - from aequilibrae.project import Project - from aequilibrae.paths.results import SkimResults + >>> from aequilibrae import Project + >>> from aequilibrae.paths.results import SkimResults + + >>> proj = Project.from_path("/tmp/test_project") + >>> proj.network.build_graphs() - proj = Project() - proj.load('path/to/project/folder') - proj.network.build_graphs() # Mode c is car in this project - car_graph = proj.network.graphs['c'] + >>> car_graph = proj.network.graphs['c'] # minimize travel time - car_graph.set_graph('free_flow_travel_time') + >>> car_graph.set_graph('free_flow_time') # Skims travel time and distance - car_graph.set_skimming(['free_flow_travel_time', 'distance']) + >>> car_graph.set_skimming(['free_flow_time', 'distance']) - res = SkimResults() - res.prepare(car_graph) - res.compute_skims() + >>> res = SkimResults() + >>> res.prepare(car_graph) - res.skims.export('path/to/matrix.aem') + >>> res.skims.export('/tmp/test_project/matrix.aem') """ def __init__(self): @@ -47,8 +46,8 @@ def prepare(self, graph: Graph): """ Prepares the object with dimensions corresponding to the graph objects - Args: - *graph* (:obj:`Graph`): Needs to have been set with number of centroids and list of skims (if any) + :Arguments: + **graph** (:obj:`Graph`): Needs to have been set with number of centroids and list of skims (if any) """ if not graph.cost_field: @@ -79,8 +78,8 @@ def set_cores(self, cores: int) -> None: Resulting number of cores will be adjusted to a minimum of zero or the maximum available in the system if the inputs result in values outside those limits - Args: - *cores* (:obj:`int`): Number of cores to be used in computation + :Arguments: + **cores** (:obj:`int`): Number of cores to be used in computation """ if isinstance(cores, int): diff --git a/aequilibrae/paths/traffic_assignment.py b/aequilibrae/paths/traffic_assignment.py index 40be72ee5..65be6fa3b 100644 --- a/aequilibrae/paths/traffic_assignment.py +++ b/aequilibrae/paths/traffic_assignment.py @@ -24,74 +24,75 @@ class TrafficAssignment(object): - """Traffic assignment class + """Traffic assignment class. For a comprehensive example on use, see the Use examples page. - :: - from os.path import join - from aequilibrae.matrix import AequilibraeMatrix - from aequilibrae.paths import TrafficAssignment, TrafficClass + .. code-block:: python + >>> from aequilibrae import Project + >>> from aequilibrae.matrix import AequilibraeMatrix + >>> from aequilibrae.paths import TrafficAssignment, TrafficClass - fldr = 'D:/release/Sample models/sioux_falls_2020_02_15' - proj_name = 'SiouxFalls.sqlite' - dt_fldr = '0_tntp_data' - prj_fldr = '1_project' + >>> project = Project.from_path("/tmp/test_project") + >>> project.network.build_graphs() - demand = AequilibraeMatrix() - demand.load(join(fldr, dt_fldr, 'demand.omx')) - demand.computational_view(['matrix']) # We will only assign one user class stored as 'matrix' inside the OMX file + >>> graph = project.network.graphs['c'] # we grab the graph for cars + >>> graph.set_graph('free_flow_time') # let's say we want to minimize time + >>> graph.set_skimming(['free_flow_time', 'distance']) # And will skim time and distance + >>> graph.set_blocked_centroid_flows(True) - project = Project() - project.load(join(fldr, prj_fldr)) - project.network.build_graphs() + >>> proj_matrices = project.matrices - graph = project.network.graphs['c'] # we grab the graph for cars - graph.set_graph('free_flow_time') # let's say we want to minimize time - graph.set_skimming(['free_flow_time', 'distance']) # And will skim time and distance - graph.set_blocked_centroid_flows(True) + >>> demand = AequilibraeMatrix() + >>> demand = proj_matrices.get_matrix("demand_omx") + + # We will only assign one user class stored as 'matrix' inside the OMX file + >>> demand.computational_view(['matrix']) # Creates the assignment class - assigclass = TrafficClass(graph, demand) + >>> assigclass = TrafficClass("car", graph, demand) + + >>> assig = TrafficAssignment() - assig = TrafficAssignment() # The first thing to do is to add at list of traffic classes to be assigned - assig.set_classes([assigclass]) + >>> assig.set_classes([assigclass]) - assig.set_vdf("BPR") # This is not case-sensitive # Then we set the volume delay function + # Then we set the volume delay function + >>> assig.set_vdf("BPR") # This is not case-sensitive - assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) # And its parameters + # And its parameters + >>> assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) - assig.set_capacity_field("capacity") # The capacity and free flow travel times as they exist in the graph - assig.set_time_field("free_flow_time") + # The capacity and free flow travel times as they exist in the graph + >>> assig.set_capacity_field("capacity") + >>> assig.set_time_field("free_flow_time") # And the algorithm we want to use to assign - assig.set_algorithm('bfw') + >>> assig.set_algorithm('bfw') - # since I haven't checked the parameters file, let's make sure convergence criteria is good - assig.max_iter = 1000 - assig.rgap_target = 0.00001 + # Since we haven't checked the parameters file, let's make sure convergence criteria is good + >>> assig.max_iter = 1000 + >>> assig.rgap_target = 0.00001 - assig.execute() # we then execute the assignment + >>> assig.execute() # we then execute the assignment - # Convergence report is here - import pandas as pd - convergence_report = pd.DataFrame(assig.assignment.convergence_report) - convergence_report.head() + # 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 - results_df = assig.results() + >>> results_df = assig.results() - # information on the assignment setup can be recovered with - info = assig.info() + # Information on the assignment setup can be recovered with + >>> info = assig.info() # Or save it directly to the results database - results = assig.save_results(table_name='example_from_the_documentation') + >>> results = assig.save_results(table_name='base_year_assignment') # skims are here - avg_skims = assigclass.results.skims # blended ones - last_skims = assigclass._aon_results.skims # those for the last iteration + >>> avg_skims = assigclass.results.skims # blended ones + >>> last_skims = assigclass._aon_results.skims # those for the last iteration """ bpr_parameters = ["alpha", "beta"] @@ -179,8 +180,8 @@ def set_vdf(self, vdf_function: str) -> None: """ Sets the Volume-delay function to be used - Args: - vdf_function(:obj:`str`:) Name of the VDF to be used + :Arguments: + **vdf_function** (:obj:`str`:) Name of the VDF to be used """ self.vdf = vdf_function @@ -188,8 +189,8 @@ def set_classes(self, classes: List[TrafficClass]) -> None: """ Sets Traffic classes to be assigned - Args: - classes (:obj:`List[TrafficClass]`:) List of Traffic classes for assignment + :Arguments: + **classes** (:obj:`List[TrafficClass]`:) List of Traffic classes for assignment """ ids = set([x.__id__ for x in classes]) @@ -201,8 +202,8 @@ def add_class(self, traffic_class: TrafficClass) -> None: """ Adds a traffic class to the assignment - Args: - traffic_class (:obj:`TrafficClass`:) Traffic class + :Arguments: + **traffic_class** (:obj:`TrafficClass`:) Traffic class """ ids = [x.__id__ for x in self.classes if x.__id__ == traffic_class.__id__] @@ -215,7 +216,7 @@ def algorithms_available(self) -> list: """ Returns all algorithms available for use - Returns: + :Returns: :obj:`list`: List of string values to be used with **set_algorithm** """ return self.all_algorithms @@ -228,8 +229,8 @@ def set_algorithm(self, algorithm: str): 'fw' is also accepted as an alternative to 'frank-wolfe' - Args: - algorithm (:obj:`list`): Algorithm to be used + :Arguments: + **algorithm** (:obj:`list`): Algorithm to be used """ # First we instantiate the arrays we will be using over and over @@ -255,8 +256,8 @@ def set_vdf_parameters(self, par: dict) -> None: Parameter values can be scalars (same values for the entire network) or network field names (link-specific values) - Examples: {'alpha': 0.15, 'beta': 4.0} or {'alpha': 'alpha', 'beta': 'beta'} - Args: - par (:obj:`dict`): Dictionary with all parameters for the chosen VDF + :Arguments: + **par** (:obj:`dict`): Dictionary with all parameters for the chosen VDF """ if self.classes is None or self.vdf.function.lower() not in all_vdf_functions: @@ -294,8 +295,8 @@ def set_cores(self, cores: int) -> None: Inherited from :obj:`AssignmentResults` - Args: - cores (:obj:`int`): Number of CPU cores to use + :Arguments: + **cores** (:obj:`int`): Number of CPU cores to use """ if not self.classes: raise Exception("You need load traffic classes before overwriting the number of cores") @@ -308,8 +309,8 @@ def set_cores(self, cores: int) -> None: def set_save_path_files(self, save_it: bool) -> None: """Turn path saving on or off. - Args: - save_it (:obj:`bool`): Boolean to indicate whether paths should be saved + :Arguments: + **save_it** (:obj:`bool`): Boolean to indicate whether paths should be saved """ if self.classes is None: raise Exception("You need to set traffic classes before turning path saving on or off") @@ -321,8 +322,8 @@ def set_save_path_files(self, save_it: bool) -> None: def set_path_file_format(self, file_format: str) -> None: """Specify path saving format. Either parquet or feather. - Args: - file_format (:obj:`str`): Name of file format to use for path files + :Arguments: + **file_format** (:obj:`str`): Name of file format to use for path files """ if self.classes is None: raise Exception("You need to set traffic classes before specifying path saving options") @@ -340,8 +341,8 @@ def set_time_field(self, time_field: str) -> None: """ Sets the graph field that contains free flow travel time -> e.g. 'fftime' - Args: - time_field (:obj:`str`): Field name + :Arguments: + **time_field** (:obj:`str`): Field name """ if not self.classes: @@ -367,8 +368,8 @@ def set_capacity_field(self, capacity_field: str) -> None: """ Sets the graph field that contains link capacity for the assignment period -> e.g. 'capacity1h' - Args: - capacity_field (:obj:`str`): Field name + :Arguments: + **capacity_field** (:obj:`str`): Field name """ if not self.classes: @@ -416,10 +417,10 @@ def save_results(self, table_name: str, keep_zero_flows=True, project=None) -> N Method fails if table exists - Args: - table_name (:obj:`str`): Name of the table to hold this assignment result - keep_zero_flows (:obj:`bool`): Whether we should keep records for zero flows. Defaults to True - project (:obj:`Project`, Optional): Project we want to save the results to. Defaults to the active project + :Arguments: + **table_name** (:obj:`str`): Name of the table to hold this assignment result + **keep_zero_flows** (:obj:`bool`): Whether we should keep records for zero flows. Defaults to True + **project** (:obj:`Project`, Optional): Project we want to save the results to. Defaults to the active project """ df = self.results() @@ -446,8 +447,8 @@ def save_results(self, table_name: str, keep_zero_flows=True, project=None) -> N def results(self) -> pd.DataFrame: """Prepares the assignment results as a Pandas DataFrame - Returns: - *DataFrame* (:obj:`pd.DataFrame`): Pandas dataframe with all the assignment results indexed on link_id + :Returns: + **DataFrame** (:obj:`pd.DataFrame`): Pandas dataframe with all the assignment results indexed on link_id """ idx = self.classes[0].graph.graph.__supernet_id__ @@ -515,8 +516,8 @@ def results(self) -> pd.DataFrame: def report(self) -> pd.DataFrame: """Returns the assignment convergence report - Returns: - *DataFrame* (:obj:`pd.DataFrame`): Convergence report + :Returns: + **DataFrame** (:obj:`pd.DataFrame`): Convergence report """ return pd.DataFrame(self.assignment.convergence_report) @@ -529,8 +530,8 @@ def info(self) -> dict: The classes key is also a dictionary with all the user classes per traffic class and their respective matrix totals - Returns: - *info* (:obj:`dict`): Pandas dataframe with all the assignment results indexed on link_id + :Returns: + **info** (:obj:`dict`): Pandas dataframe with all the assignment results indexed on link_id """ classes = {} @@ -568,12 +569,12 @@ def info(self) -> dict: def save_skims(self, matrix_name: str, which_ones="final", format="omx", project=None) -> None: """Saves the skims (if any) to the skim folder and registers in the matrix list - Args: - name (:obj:`str`): Name of the matrix record to hold this matrix (same name used for file name) - which_ones (:obj:`str`,optional): {'final': Results of the final iteration, 'blended': Averaged results for + :Arguments: + **name** (:obj:`str`): Name of the matrix record to hold this matrix (same name used for file name) + **which_ones** (:obj:`str`,optional): {'final': Results of the final iteration, 'blended': Averaged results for all iterations, 'all': Saves skims for both the final iteration and the blended ones} Default is 'final' - *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 + **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 """ mat_format = format.lower() if mat_format not in ["omx", "aem"]: @@ -682,9 +683,11 @@ def save_select_link_flows(self, table_name: str, project=None) -> None: """ Saves the select link link flows for all classes into the results database. Additionally, it exports the OD matrices into OMX format. - Args: - str table_name: Name of the table being inserted to. Note the traffic class - project (:obj:`Project`, Optional): Project we want to save the results to. Defaults to the active project + + :Arguments: + **table_name** (:obj:`str`): Name of the table being inserted to. Note the traffic class + **project** (:obj:`Project`, `Optional`): Project we want to save the results to. + Defaults to the active project """ if not project: @@ -728,13 +731,17 @@ def save_select_link_matrices(self, file_name: str) -> None: def save_select_link_results(self, name: str) -> None: """ Saves both the Select Link matrices and flow results at the same time, using the same name. - Note the Select Link matrices will have _SL_matrices.omx appended to the end for ease of identification. - e.g. save_select_link_results("Car") will result in the following names for the flows and matrices: - Select Link Flows: inserts the select link flows for each class into the database with the table name: - Car - Select Link Matrices (only exports to OMX format): - Car.omx + .. note:: + Note the Select Link matrices will have _SL_matrices.omx appended to the end for ease of identification. + e.g. save_select_link_results("Car") will result in the following names for the flows and matrices: + Select Link Flows: inserts the select link flows for each class into the database with the table name: + Car + Select Link Matrices (only exports to OMX format): + Car.omx + + :Arguments: + **name** (:obj:`str`): name of the matrices """ self.save_select_link_flows(name) self.save_select_link_matrices(name) diff --git a/aequilibrae/paths/traffic_class.py b/aequilibrae/paths/traffic_class.py index 60f73ee5c..68bd15fc5 100644 --- a/aequilibrae/paths/traffic_class.py +++ b/aequilibrae/paths/traffic_class.py @@ -11,24 +11,40 @@ class TrafficClass: """Traffic class for equilibrium traffic assignment - :: + .. code-block:: python - from aequilibrae.paths import TrafficClass + >>> from aequilibrae import Project + >>> from aequilibrae.matrix import AequilibraeMatrix + >>> from aequilibrae.paths import TrafficClass - tc = TrafficClass(graph, demand_matrix) - tc.set_pce(1.3) + >>> project = Project.from_path("/tmp/test_project") + >>> project.network.build_graphs() + + >>> graph = project.network.graphs['c'] # we grab the graph for cars + >>> graph.set_graph('free_flow_time') # let's say we want to minimize time + >>> graph.set_skimming(['free_flow_time', 'distance']) # And will skim time and distance + >>> graph.set_blocked_centroid_flows(True) + + >>> proj_matrices = project.matrices + + >>> demand = AequilibraeMatrix() + >>> demand = proj_matrices.get_matrix("demand_omx") + >>> demand.computational_view(['matrix']) + + >>> tc = TrafficClass("car", graph, demand) + >>> tc.set_pce(1.3) """ def __init__(self, name: str, graph: Graph, matrix: AequilibraeMatrix) -> None: """ Instantiates the class - Args: - name (:obj:`str`): UNIQUE class name. + :Arguments: + **name** (:obj:`str`): UNIQUE class name. - graph (:obj:`Graph`): Class/mode-specific graph + **graph** (:obj:`Graph`): Class/mode-specific graph - matrix (:obj:`AequilibraeMatrix`): Class/mode-specific matrix. Supports multiple user classes + **matrix** (:obj:`AequilibraeMatrix`): Class/mode-specific matrix. Supports multiple user classes """ if not np.array_equal(matrix.index, graph.centroids): raise ValueError("Matrix and graph do not have compatible sets of centroids.") @@ -54,8 +70,8 @@ def __init__(self, name: str, graph: Graph, matrix: AequilibraeMatrix) -> None: def set_pce(self, pce: Union[float, int]) -> None: """Sets Passenger Car equivalent - Args: - pce (:obj:`Union[float, int]`): PCE. Defaults to 1 if not set + :Arguments: + **pce** (:obj:`Union[float, int]`): PCE. Defaults to 1 if not set """ if not isinstance(pce, (float, int)): raise ValueError("PCE needs to be either integer or float ") @@ -64,9 +80,9 @@ def set_pce(self, pce: Union[float, int]) -> None: def set_fixed_cost(self, field_name: str, multiplier=1): """Sets value of time - Args: - field_name (:obj:`str`): Name of the graph field with fixed costs for this class - multiplier (:obj:`Union[float, int]`): Multiplier for the fixed cost. Defaults to 1 if not set + :Arguments: + **field_name** (:obj:`str`): Name of the graph field with fixed costs for this class + **multiplier** (:obj:`Union[float, int]`): Multiplier for the fixed cost. Defaults to 1 if not set """ if field_name not in self.graph.graph.columns: raise ValueError("Field does not exist in the graph") @@ -84,8 +100,8 @@ def set_fixed_cost(self, field_name: str, multiplier=1): def set_vot(self, value_of_time: float) -> None: """Sets value of time - Args: - value_of_time (:obj:`Union[float, int]`): Value of time. Defaults to 1 if not set + :Arguments: + **value_of_time** (:obj:`Union[float, int]`): Value of time. Defaults to 1 if not set """ self.vot = float(value_of_time) @@ -95,8 +111,8 @@ def set_select_links(self, links: Dict[str, List[Tuple[int, int]]]): direction into unique link id used in compact graph. Supply links=None to disable select link analysis. - Args: - links (:obj:`Union[None, Dict[str, List[Tuple[int, int]]]]`): name of link set and + :Arguments: + **links** (:obj:`Union[None, Dict[str, List[Tuple[int, int]]]]`): name of link set and Link IDs and directions to be used in select link analysis""" self._selected_links = {} for name, link_set in links.items(): diff --git a/aequilibrae/paths/vdf.py b/aequilibrae/paths/vdf.py index a449cd10b..6d173861d 100644 --- a/aequilibrae/paths/vdf.py +++ b/aequilibrae/paths/vdf.py @@ -11,13 +11,13 @@ class VDF: """Volume-Delay function - :: + .. code-block:: python - from aequilibrae.paths import VDF + >>> from aequilibrae.paths import VDF - vdf = VDF() - vdf.functions_available() - ['bpr', 'bpr2', 'conical', 'inrets'] + >>> vdf = VDF() + >>> vdf.functions_available() + ['bpr', 'bpr2', 'conical', 'inrets'] """ diff --git a/aequilibrae/project/about.py b/aequilibrae/project/about.py index 82a29fe96..f7ffd1471 100644 --- a/aequilibrae/project/about.py +++ b/aequilibrae/project/about.py @@ -6,15 +6,21 @@ class About: """Provides an interface for querying and editing the **about** table of an AequilibraE project - :: - p = Project() - p.open('my/project/folder') - about = p.about + .. code-block:: python - about.description = 'This is the example project. Do not use for forecast' - about.write_back() + >>> from aequilibrae import Project + >>> project = Project.from_path("/tmp/test_project") + + # Adding a new field and saving it + >>> project.about.add_info_field('my_super_relevant_field') + >>> project.about.my_super_relevant_field = 'super relevant information' + >>> project.about.write_back() + + # changing the value for an existing value/field + >>> project.about.scenario_name = 'Just a better scenario name' + >>> project.about.write_back() """ @@ -53,17 +59,18 @@ def list_fields(self) -> list: def add_info_field(self, info_field: str) -> None: """Adds new information field to the model - Args: - *info_field* (:obj:`str`): Name of the desired information field to be added. Has to be a valid + :Arguments: + **info_field** (:obj:`str`): Name of the desired information field to be added. Has to be a valid Python VARIABLE name (i.e. letter as first character, no spaces and no special characters) - :: + .. code-block:: python - p = Project() - p.open('my/project/folder') - p.about.add_info_field('my_super_relevant_field') - p.about.my_super_relevant_field = 'super relevant information' - p.about.write_back() + >>> from aequilibrae import Project + + >>> 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() """ allowed = string.ascii_lowercase + "_" has_forbidden = [x for x in info_field if x not in allowed] @@ -81,12 +88,13 @@ def add_info_field(self, info_field: str) -> None: def write_back(self): """Saves the information parameters back to the project database - :: + .. code-block:: python + + >>> from aequilibrae import Project - p = Project() - p.open('my/project/folder') - p.about.description = 'This is the example project. Do not use for forecast' - p.about.write_back() + >>> p = Project.from_path("/tmp/test_project") + >>> p.about.description = 'This is the example project. Do not use for forecast' + >>> p.about.write_back() """ curr = self.__conn.cursor() for k in self.__characteristics: diff --git a/aequilibrae/project/database_specification/network/tables/about.sql b/aequilibrae/project/database_specification/network/tables/about.sql index 4b80b5bcb..daa3092b8 100644 --- a/aequilibrae/project/database_specification/network/tables/about.sql +++ b/aequilibrae/project/database_specification/network/tables/about.sql @@ -1,3 +1,11 @@ +--@ The *about* table holds information about the AequilibraE model +--@ currently developed. +--@ +--@ The **infoname** field holds the name of information being added +--@ +--@ The **infovalue** field holds the information to add + + CREATE TABLE if not exists about (infoname TEXT UNIQUE NOT NULL, infovalue TEXT ); diff --git a/aequilibrae/project/database_specification/network/tables/attribute_documentation.sql b/aequilibrae/project/database_specification/network/tables/attribute_documentation.sql index 9129217c0..779c503b3 100644 --- a/aequilibrae/project/database_specification/network/tables/attribute_documentation.sql +++ b/aequilibrae/project/database_specification/network/tables/attribute_documentation.sql @@ -1,3 +1,20 @@ +--@ The *attributes_documentation* table holds information about attributes +--@ in the tables links, link_types, modes, nodes, and zones. +--@ +--@ By default, these attributes are all documented, but further +--@ attribues can be added into the table. +--@ +--@ The **name_table** field holds the name of the table that has the attribute +--@ +--@ The **attribute** field holds the name of the attribute +--@ +--@ The **description** field holds the description of the attribute +--@ +--@ It is possible to have one attribute with the same name in two +--@ different tables. However, one cannot have two attibutes with the +--@ same name within the same table. + + CREATE TABLE if not exists attributes_documentation (name_table TEXT NOT NULL, attribute TEXT NOT NULL, description TEXT, diff --git a/aequilibrae/project/database_specification/network/tables/link_types.sql b/aequilibrae/project/database_specification/network/tables/link_types.sql index e33f8db34..f5401aaf0 100644 --- a/aequilibrae/project/database_specification/network/tables/link_types.sql +++ b/aequilibrae/project/database_specification/network/tables/link_types.sql @@ -1,3 +1,20 @@ +--@ The *link_types* table holds information about the available +--@ link types in the network. +--@ +--@ The **link_type** field corresponds to the link type, and it is the +--@ table's primary key +--@ +--@ The **link_type_id** field presents the identification of the link type +--@ +--@ The **description** field holds the description of the link type +--@ +--@ The **lanes** field presents the number or lanes for the link type +--@ +--@ The **lane_capacity** field presents the number of lanes for the link type +--@ +--@ The **speed** field holds information about the speed in the link type + + CREATE TABLE if not exists link_types (link_type VARCHAR UNIQUE NOT NULL PRIMARY KEY, link_type_id VARCHAR UNIQUE NOT NULL, description VARCHAR, diff --git a/aequilibrae/project/database_specification/network/tables/links.sql b/aequilibrae/project/database_specification/network/tables/links.sql index 314ecd6f4..d52ed42b6 100644 --- a/aequilibrae/project/database_specification/network/tables/links.sql +++ b/aequilibrae/project/database_specification/network/tables/links.sql @@ -1,3 +1,26 @@ +--@ The links table holds all the links available in the aequilibrae network model +--@ regardless of the modes allowed on it. +--@ +--@ All information on the fields a_node and b_node correspond to a entries in +--@ the node_id field in the nodes table. They are automatically managed with +--@ triggers as the user edits the network, but they are not protected by manual +--@ editing, which would break the network if it were to happen. +--@ +--@ The **modes** field is a concatenation of all the ids (mode_id) of the models allowed +--@ on each link, and map directly to the mode_id field in the **Modes** table. A mode +--@ can only be added to a link if it exists in the **Modes** table. +--@ +--@ The **link_type** corresponds to the *link_type* field from the *link_types* table. +--@ As it is the case for modes, a link_type can only be assigned to a link if it exists +--@ in the **link_types** table. +--@ +--@ The fields **length**, **node_a** and **node_b** are automatically +--@ updated by triggers based in the links' geometries and node positions. Link length +--@ is always measured in **meters**. +--@ +--@ The table is indexed on **link_id** (its primary key), **node_a** and **node_b**. + + CREATE TABLE if not exists links (ogc_fid INTEGER PRIMARY KEY, link_id INTEGER NOT NULL UNIQUE, a_node INTEGER, diff --git a/aequilibrae/project/database_specification/network/tables/matrices.sql b/aequilibrae/project/database_specification/network/tables/matrices.sql index 8d941ee5c..2e42de392 100644 --- a/aequilibrae/project/database_specification/network/tables/matrices.sql +++ b/aequilibrae/project/database_specification/network/tables/matrices.sql @@ -1,3 +1,23 @@ +--@ The *matrices* table holds infromation about all matrices that exists in the +--@ project *matrix* folder. +--@ +--@ The **name** field presents the name of the table. +--@ +--@ The **file_name** field holds the file name. +--@ +--@ The **cores** field holds the information on the number of cores used. +--@ +--@ The **procedure** field holds the name the the procedure that generated +--@ the result (e.g.: Traffic Assignment). +--@ +--@ The **procedure_id** field holds an unique alpha-numeric identifier for +--@ this prodecure. +--@ +--@ The **timestamp** field holds the information when the procedure was executed. +--@ +--@ The **description** field holds the user-provided description of the result. + + create TABLE if not exists matrices (name TEXT NOT NULL PRIMARY KEY, file_name TEXT NOT NULL UNIQUE, cores INTEGER NOT NULL DEFAULT 1, diff --git a/aequilibrae/project/database_specification/network/tables/modes.sql b/aequilibrae/project/database_specification/network/tables/modes.sql index db1e7a186..a82ba25af 100644 --- a/aequilibrae/project/database_specification/network/tables/modes.sql +++ b/aequilibrae/project/database_specification/network/tables/modes.sql @@ -1,3 +1,22 @@ +--@ The *modes* table holds the information on all the modes available in +--@ the model's network. +--@ +--@ The **mode_name** field contains the descriptive name of the field. +--@ +--@ The **mode_id** field contains a single letter that identifies the mode. +--@ +--@ The **description** field holds the description of the mode. +--@ +--@ The **pce** field holds information on Passenger-Car equivalent +--@ for assignment. Defaults to **1.0**. +--@ +--@ The **vot** field holds information on Value-of-Time for traffic +--@ assignment. Defaults to **0.0**. +--@ +--@ The **ppv** field holds information on average persons per vehicle. +--@ Defaults to **1.0**. **ppv** can assume value 0 for non-travel uses. + + CREATE TABLE if not exists modes (mode_name VARCHAR UNIQUE NOT NULL, mode_id VARCHAR UNIQUE NOT NULL PRIMARY KEY, description VARCHAR, diff --git a/aequilibrae/project/database_specification/network/tables/nodes.sql b/aequilibrae/project/database_specification/network/tables/nodes.sql index 20192cf62..ba63fc1c5 100644 --- a/aequilibrae/project/database_specification/network/tables/nodes.sql +++ b/aequilibrae/project/database_specification/network/tables/nodes.sql @@ -1,3 +1,16 @@ +--@ The *nodes* table holds all the network nodes available in AequilibraE model. +--@ +--@ The **node_id** field is an identifier of the node. +--@ +--@ The **is_centroid** field holds information if the node is a centroid +--@ of a network or not. Assumes values 0 or 1. Defaults to **0**. +--@ +--@ The **modes** field identifies all modes connected to the node. +--@ +--@ The **link_types** field identifies all link types connected +--@ to the node. + + CREATE TABLE if not exists nodes (ogc_fid INTEGER PRIMARY KEY, node_id INTEGER UNIQUE NOT NULL, is_centroid INTEGER NOT NULL DEFAULT 0, diff --git a/aequilibrae/project/database_specification/network/tables/results.sql b/aequilibrae/project/database_specification/network/tables/results.sql index caf5b229e..cba8eb7d1 100644 --- a/aequilibrae/project/database_specification/network/tables/results.sql +++ b/aequilibrae/project/database_specification/network/tables/results.sql @@ -1,3 +1,22 @@ +--@ The *results* table holds the metadata for results stored in +--@ *results_database.sqlite*. +--@ +--@ The **table_name** field presents the actual name of the result +--@ table in *results_database.sqlite*. +--@ +--@ The **procedure** field holds the name the the procedure that generated +--@ the result (e.g.: Traffic Assignment). +--@ +--@ The **procedure_id** field holds an unique UUID identifier for this procedure, +--@ which is created at runtime. +--@ +--@ The **procedure_report** field holds the output of the complete procedure report. +--@ +--@ The **timestamp** field holds the information when the procedure was executed. +--@ +--@ The **description** field holds the user-provided description of the result. + + create TABLE if not exists results (table_name TEXT NOT NULL PRIMARY KEY, procedure TEXT NOT NULL, procedure_id TEXT NOT NULL, diff --git a/aequilibrae/project/database_specification/network/tables/zones.sql b/aequilibrae/project/database_specification/network/tables/zones.sql index ac00aadc4..ab55fb52e 100644 --- a/aequilibrae/project/database_specification/network/tables/zones.sql +++ b/aequilibrae/project/database_specification/network/tables/zones.sql @@ -1,9 +1,18 @@ +--@ The *zones* table holds information on the Traffic Analysis Zones (TAZs) +--@ in AequilibraE's model. +--@ +--@ The **zone_id** field identifies the zone. +--@ +--@ The **area** field corresponds to the area of the zone in **km2**. +--@ TAZs' area is automatically updated by triggers. +--@ +--@ The **name** fields allows one to identity the zone using a name +--@ or any other description. + CREATE TABLE 'zones' (ogc_fid INTEGER PRIMARY KEY, zone_id INTEGER UNIQUE NOT NULL, area NUMERIC, - "name" TEXT, - population INTEGER, - employment INTEGER); + "name" TEXT); --# SELECT AddGeometryColumn( 'zones', 'geometry', 4326, 'MULTIPOLYGON', 'XY', 1); @@ -17,7 +26,3 @@ INSERT INTO 'attributes_documentation' (name_table, attribute, description) VALU INSERT INTO 'attributes_documentation' (name_table, attribute, description) VALUES('zones','area', 'Area of the zone in km2'); --# INSERT INTO 'attributes_documentation' (name_table, attribute, description) VALUES('zones','name', 'Name of the zone, if any'); ---# -INSERT INTO 'attributes_documentation' (name_table, attribute, description) VALUES('zones','population', "Zone's total population"); ---# -INSERT INTO 'attributes_documentation' (name_table, attribute, description) VALUES('zones','employment', "Zone's total employment"); diff --git a/aequilibrae/project/database_specification/transit/tables/agencies.sql b/aequilibrae/project/database_specification/transit/tables/agencies.sql index d11103cb8..dcca9e9ea 100644 --- a/aequilibrae/project/database_specification/transit/tables/agencies.sql +++ b/aequilibrae/project/database_specification/transit/tables/agencies.sql @@ -1,3 +1,18 @@ +--@ The *agencies* table holds information about the Public Transport +--@ agencies within the GTFS data. This table information comes from +--@ GTFS file *agency.txt*. +--@ You can check out more information `here `_. +--@ +--@ **agency_id** identifies the agency for the specified route +--@ +--@ **agency** contains the fuill name of the transit agency +--@ +--@ **feed_date** idicates the date for which the GTFS feed is being imported +--@ +--@ **service_date** indicates the date for the indicate route scheduling +--@ +--@ **description_field** provides useful description of a transit agency + create TABLE IF NOT EXISTS agencies ( agency_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, agency TEXT NOT NULL, diff --git a/aequilibrae/project/database_specification/transit/tables/fare_attributes.sql b/aequilibrae/project/database_specification/transit/tables/fare_attributes.sql index e7ef08972..bcf9849bb 100644 --- a/aequilibrae/project/database_specification/transit/tables/fare_attributes.sql +++ b/aequilibrae/project/database_specification/transit/tables/fare_attributes.sql @@ -1,3 +1,25 @@ +--@ The *fare_attributes* table holds information about the fare values. +--@ This table information comes from the GTFS file *fare_attributes.txt*. +--@ Given that this file is optional in GTFS, it can be empty. +--@ You can check out more information `here `_. +--@ +--@ **fare_id** identifies a fare class +--@ +--@ **fare** describes a fare class +--@ +--@ **agency_id** identifies a relevant agency for a fare. +--@ +--@ **price** especifies the fare price +--@ +--@ **currency_code** especifies the currency used to pay the fare +--@ +--@ **payment_method** indicates when the fare must be paid. +--@ +--@ **transfer** indicates the number of transfers permitted on the fare +--@ +--@ **transfer_duration** indicates the lenght of time in seconds before a +--@ transfer expires. + create TABLE IF NOT EXISTS fare_attributes ( fare_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, fare TEXT NOT NULL, diff --git a/aequilibrae/project/database_specification/transit/tables/fare_rules.sql b/aequilibrae/project/database_specification/transit/tables/fare_rules.sql index 431784947..dee7d708f 100644 --- a/aequilibrae/project/database_specification/transit/tables/fare_rules.sql +++ b/aequilibrae/project/database_specification/transit/tables/fare_rules.sql @@ -1,3 +1,18 @@ +--@ The *fare_rules* table holds information about the fare values. +--@ This table information comes from the GTFS file *fare_rules.txt*. +--@ Given that this file is optional in GTFS, it can be empty. +--@ +--@ The **fare_id** identifies a fare class +--@ +--@ The **route_id** identifies a route associated with the fare class. +--@ +--@ The **origin** field identifies the origin zone +--@ +--@ The **destination** field identifies the destination zone +--@ +--@ The **contains** field identifies the zones that a rider will enter while using +--@ a given fare class. + create TABLE IF NOT EXISTS fare_rules ( fare_id INTEGER NOT NULL, route_id INTEGER, diff --git a/aequilibrae/project/database_specification/transit/tables/fare_zones.sql b/aequilibrae/project/database_specification/transit/tables/fare_zones.sql index d55069826..b5f8ded58 100644 --- a/aequilibrae/project/database_specification/transit/tables/fare_zones.sql +++ b/aequilibrae/project/database_specification/transit/tables/fare_zones.sql @@ -1,3 +1,12 @@ +--@ The *zones* tables holds information on the fare transit zones and +--@ the TAZs they are in. +--@ +--@ **fare_zone_id** identifies the fare zone for a stop +--@ +--@ **transit_zone** identifies the TAZ for a fare zone +--@ +--@ **agency_id** identifies the agency fot the specified route + CREATE TABLE IF NOT EXISTS fare_zones ( fare_zone_id INTEGER PRIMARY KEY, transit_zone TEXT NOT NULL, diff --git a/aequilibrae/project/database_specification/transit/tables/pattern_mapping.sql b/aequilibrae/project/database_specification/transit/tables/pattern_mapping.sql index 23d50168a..8ec0e6b40 100644 --- a/aequilibrae/project/database_specification/transit/tables/pattern_mapping.sql +++ b/aequilibrae/project/database_specification/transit/tables/pattern_mapping.sql @@ -1,3 +1,15 @@ +--@ The *pattern_mapping* table holds information on the stop pattern +--@ for each route. +--@ +--@ **pattern_id** is an unique pattern for the route +--@ +--@ **seq** identifies the sequence of the stops for a trip +--@ +--@ **link** identifies the *link_id* in the links table that corresponds to the +--@ pattern matching +--@ +--@ **dir** indicates the direction of travel for a trip + CREATE TABLE IF NOT EXISTS pattern_mapping ( pattern_id INTEGER NOT NULL, seq INTEGER NOT NULL, diff --git a/aequilibrae/project/database_specification/transit/tables/route_links.sql b/aequilibrae/project/database_specification/transit/tables/route_links.sql index d8560046a..fbd159d6b 100644 --- a/aequilibrae/project/database_specification/transit/tables/route_links.sql +++ b/aequilibrae/project/database_specification/transit/tables/route_links.sql @@ -1,3 +1,18 @@ +--@ The *route_links* table holds information on the links of a route. +--@ +--@ **transit_link** identifies the GTFS transit links for the route +--@ +--@ **pattern_id** is an unique pattern for the route +--@ +--@ **seq** identifies the sequence of the stops for a trip +--@ +--@ **from_stop** identifies the stop the vehicle is departing +--@ +--@ **to_stop** identifies the next stop the vehicle is going to arrive +--@ +--@ **distance** identifies the distance (in meters) the vehicle travel +--@ between the stops + CREATE TABLE IF NOT EXISTS route_links ( transit_link INTEGER NOT NULL, pattern_id INTEGER NOT NULL, diff --git a/aequilibrae/project/database_specification/transit/tables/routes.sql b/aequilibrae/project/database_specification/transit/tables/routes.sql index 21be990c1..f60cf2361 100644 --- a/aequilibrae/project/database_specification/transit/tables/routes.sql +++ b/aequilibrae/project/database_specification/transit/tables/routes.sql @@ -1,3 +1,27 @@ +--@ The *routes* table holds information on the available transit routes for a +--@ specific day. This table information comes from the GTFS file *routes.txt*. +--@ You can find more information about it `here `_. +--@ +--@ **pattern_id** is an unique pattern for the route +--@ +--@ **route_id** identifies a route +--@ +--@ **route** identifies the name of a route +--@ +--@ **agency_id** identifies the agency for the specified route +--@ +--@ **shortname** identifies the short name of a route +--@ +--@ **longname** identifies the long name of a route +--@ +--@ **description** provides useful description of a route +--@ +--@ **route_type** indicates the type of transporation used on a route +--@ +--@ **seated_capacity** indicates the seated capacity of a route +--@ +--@ **total_capacity** indicates the total capacity of a route + CREATE TABLE IF NOT EXISTS routes ( pattern_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, route_id INTEGER NOT NULL, diff --git a/aequilibrae/project/database_specification/transit/tables/stop_connectors.sql b/aequilibrae/project/database_specification/transit/tables/stop_connectors.sql index 9587fac2c..f86b4ea24 100644 --- a/aequilibrae/project/database_specification/transit/tables/stop_connectors.sql +++ b/aequilibrae/project/database_specification/transit/tables/stop_connectors.sql @@ -1,3 +1,17 @@ +--@ The *stops_connectors* table holds information on the connection of +--@ the GTFS network with the real network. +--@ +--@ **id_from** identifies the network link the vehicle departs +--@ +--@ **id_to** identifies the network link th vehicle is heading to +--@ +--@ **conn_type** identifies the type of connection used to connect the links +--@ +--@ **traversal_time** represents the time spent crossing the link +--@ +--@ **penalty_cost** identifies the penalty in the connection + + CREATE TABLE IF NOT EXISTS stop_connectors ( id_from INTEGER NOT NULL, id_to INTEGER NOT NULL, diff --git a/aequilibrae/project/database_specification/transit/tables/stops.sql b/aequilibrae/project/database_specification/transit/tables/stops.sql index 3ea7fa5bb..f63a8dddf 100644 --- a/aequilibrae/project/database_specification/transit/tables/stops.sql +++ b/aequilibrae/project/database_specification/transit/tables/stops.sql @@ -1,3 +1,34 @@ +--@ The *stops* table holds information on the stops where vehicles +--@ pick up or drop off riders. This table information comes from +--@ the GTFS file *stops.txt*. You can find more information about +--@ it `here `_. +--@ +--@ **stop_id** is an unique identifier for a stop +--@ +--@ **stop** idenfifies a stop, statio, or station entrance +--@ +--@ **agency_id** identifies the agency fot the specified route +--@ +--@ **link** identifies the *link_id* in the links table that corresponds to the +--@ pattern matching +--@ +--@ **dir** indicates the direction of travel for a trip +--@ +--@ **name** identifies the name of a stop +--@ +--@ **parent_station** defines hierarchy between different locations +--@ defined in *stops.txt*. +--@ +--@ **description** provides useful description of the stop location +--@ +--@ **street** identifies the address of a stop +--@ +--@ **fare_zone_id** identifies the fare zone for a stop +--@ +--@ **transit_zone** identifies the TAZ for a fare zone +--@ +--@ **route_type** indicates the type of transporation used on a route + CREATE TABLE IF NOT EXISTS stops ( stop_id INTEGER PRIMARY KEY AUTOINCREMENT , stop TEXT NOT NULL , diff --git a/aequilibrae/project/database_specification/transit/tables/trips.sql b/aequilibrae/project/database_specification/transit/tables/trips.sql index 3e9dbea8b..55b64fc38 100644 --- a/aequilibrae/project/database_specification/transit/tables/trips.sql +++ b/aequilibrae/project/database_specification/transit/tables/trips.sql @@ -1,3 +1,15 @@ +--@ The *trips* table holds information on trips for each route. +--@ This table comes from the GTFS file *trips.txt*. +--@ You can find more information about it `here `_. +--@ +--@ **trip_id** identifies a trip +--@ +--@ **trip** identifies the trip to a rider +--@ +--@ **dir** indicates the direction of travel for a trip +--@ +--@ **pattern_id** is an unique pattern for the route + CREATE TABLE IF NOT EXISTS trips ( trip_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, trip TEXT, diff --git a/aequilibrae/project/database_specification/transit/tables/trips_schedule.sql b/aequilibrae/project/database_specification/transit/tables/trips_schedule.sql index ec7990567..43985b3e5 100644 --- a/aequilibrae/project/database_specification/transit/tables/trips_schedule.sql +++ b/aequilibrae/project/database_specification/transit/tables/trips_schedule.sql @@ -1,3 +1,14 @@ +--@ The *trips_schedule* table holds information on the sequence of stops +--@ of a trip. +--@ +--@ **trip_id** is an unique identifier of a trip +--@ +--@ **seq** identifies the sequence of the stops for a trip +--@ +--@ **arrival** identifies the arrival time at the stop +--@ +--@ **departure** identifies the departure time at the stop + CREATE TABLE IF NOT EXISTS trips_schedule ( trip_id INTEGER NOT NULL, seq INTEGER NOT NULL, diff --git a/aequilibrae/project/field_editor.py b/aequilibrae/project/field_editor.py index 05f0eae6d..12bd77b4c 100644 --- a/aequilibrae/project/field_editor.py +++ b/aequilibrae/project/field_editor.py @@ -17,18 +17,17 @@ class FieldEditor: to the user and but it should be accessed directly from within the module corresponding to the data table one wants to edit. Example: - :: + .. code-block:: python - from aequilibrae import Project + >>> from aequilibrae import Project - proj = Project() - proj.open('Path/to/project/folder') + >>> proj = Project.from_path("/tmp/test_project") # To edit the fields of the link_types table - lt_fields = proj.network.link_types.fields + >>> lt_fields = proj.network.link_types.fields # To edit the fields of the modes table - m_fields = proj.network.modes.fields + >>> m_fields = proj.network.modes.fields Field descriptions are kept in the table *attributes_documentation* """ @@ -57,10 +56,10 @@ def _populate(self): def add(self, field_name: str, description: str, data_type="NUMERIC") -> None: """Adds new field to the data table - Args: - *field_name* (:obj:`str`): Field to be added to the table. Must be a valid SQLite field name - *description* (:obj:`str`): Description of the field to be inserted in the metadata - *data_type* (:obj:`str`, optional): Valid SQLite Data type. Default: "NUMERIC" + :Arguments: + **field_name** (:obj:`str`): Field to be added to the table. Must be a valid SQLite field name + **description** (:obj:`str`): Description of the field to be inserted in the metadata + **data_type** (:obj:`str`, optional): Valid SQLite Data type. Default: "NUMERIC" """ if field_name.lower() in self._original_values.keys(): raise ValueError("attribute_name already exists") diff --git a/aequilibrae/project/network/gmns_exporter.py b/aequilibrae/project/network/gmns_exporter.py index d851092e7..6c7e35ffb 100644 --- a/aequilibrae/project/network/gmns_exporter.py +++ b/aequilibrae/project/network/gmns_exporter.py @@ -83,7 +83,7 @@ def update_direction_field(self): self.links_df.loc[idx, "dir_flag"] = 1 def update_field_names(self): - """' + """ Updates field names according to equivalency between AequilibraE and GMNS fields. """ diff --git a/aequilibrae/project/network/link.py b/aequilibrae/project/network/link.py index 32a191172..1037c21eb 100644 --- a/aequilibrae/project/network/link.py +++ b/aequilibrae/project/network/link.py @@ -6,44 +6,41 @@ class Link(SafeClass): """A Link object represents a single record in the *links* table - :: + .. code-block:: python - from aequilibrae import Project + >>> from aequilibrae import Project - proj = Project() - proj.open('path/to/project/folder') + >>> proj = Project.from_path("/tmp/test_project") - all_links = proj.network.links + >>> all_links = proj.network.links # Let's get a mode to work with - modes = proj.network.modes - car_mode = modes.get('c') + >>> modes = proj.network.modes + >>> car_mode = modes.get('c') # We can just get one link in specific - link1 = all_links.get(4523) - link2 = all_links.get(3254) + >>> link1 = all_links.get(3) + >>> link2 = all_links.get(17) # We can find out which fields exist for the links - which_fields_do_we_have = link1.data_fields() + >>> which_fields_do_we_have = link1.data_fields() # And edit each one like this - link1.lanes_ab = 3 - link1.lanes_ba = 2 + >>> link1.lanes_ab = 3 + >>> link1.lanes_ba = 2 # we can drop a mode from the link - link1.drop_mode(car_mode) - # or link1.drop_mode('c') + >>> link1.drop_mode(car_mode) # or link1.drop_mode('c') # we can add a mode to the link - link2.add_mode(car_mode) - # or link2.add_mode('c') + >>> link2.add_mode(car_mode) # or link2.add_mode('c') # Or set all modes at once - link2.set_modes('cmtw') + >>> link2.set_modes('cbtw') # We can just save the link - link1.save() - link2.save() + >>> link1.save() + >>> link2.save() """ def __init__(self, dataset, project): @@ -85,8 +82,8 @@ def save(self): def set_modes(self, modes: str): """Sets the modes acceptable for this link - Args: - *modes* (:obj:`str`): string with all mode_ids to be assigned to this link + :Arguments: + **modes** (:obj:`str`): string with all mode_ids to be assigned to this link """ if not isinstance(modes, str): @@ -101,8 +98,8 @@ def add_mode(self, mode: Union[str, Mode]): Raises a warning if mode is already allowed on the link, and fails if mode does not exist - Args: - *mode_id* (:obj:`str` or `Mode`): Mode_id of the mode or mode object to be added to the link + :Arguments: + **mode_id** (:obj:`str` or `Mode`): Mode_id of the mode or mode object to be added to the link """ mode_id = self.__validate(mode) @@ -117,8 +114,8 @@ def drop_mode(self, mode: Union[str, Mode]): Raises a warning if mode is already NOT allowed on the link, and fails if mode does not exist - Args: - *mode_id* (:obj:`str` or `Mode`): Mode_id of the mode or mode object to be removed from the link + :Arguments: + **mode_id** (:obj:`str` or `Mode`): Mode_id of the mode or mode object to be removed from the link """ mode_id = self.__validate(mode) @@ -135,8 +132,8 @@ def drop_mode(self, mode: Union[str, Mode]): def data_fields(self) -> list: """lists all data fields for the link, as available in the database - Returns: - *data fields* (:obj:`list`): list of all fields available for editing + :Returns: + **data fields** (:obj:`list`): list of all fields available for editing """ return list(self.__original__.keys()) diff --git a/aequilibrae/project/network/link_types.py b/aequilibrae/project/network/link_types.py index 8986abd53..2a636a8be 100644 --- a/aequilibrae/project/network/link_types.py +++ b/aequilibrae/project/network/link_types.py @@ -6,54 +6,52 @@ class LinkTypes: """ - Access to the API resources to manipulate the link_types table in the network + Access to the API resources to manipulate the link_types table in the network. - :: + .. code-block:: python - from aequilibrae import Project + >>> from aequilibrae import Project - p = Project() - p.open('path/to/project/folder') + >>> p = Project.from_path("/tmp/test_project") - link_types = p.network.link_types + >>> link_types = p.network.link_types # We can get a dictionary of link types in the model - all_link_types = link_types.all_types() + >>> all_link_types = link_types.all_types() - #And do a bulk change and save it - for link_type_id, link_type_obj in all_link_types.items(): - link_type_obj.beta = 1 + # And do a bulk change and save it + >>> for link_type_id, link_type_obj in all_link_types.items(): + ... link_type_obj.beta = 1 # We can save changes for all link types in one go - all_link_types.save() + >>> link_types.save() # or just get one link_type in specific - default_link_type = link_types.get('y') + >>> default_link_type = link_types.get('y') # or just get it by name - default_link_type = link_types.get_by_name('default') + >>> default_link_type = link_types.get_by_name('default') # We can change the description of the link types - default_link_type.description = 'My own new description' + >>> default_link_type.description = 'My own new description' # Let's say we are using alpha to store lane capacity during the night as 90% of the standard - default_link_type.alpha =0.9 * default_link_type.lane_capacity + >>> default_link_type.alpha = 0.9 * default_link_type.lane_capacity # To save this link types we can simply - default_link_type.save() + >>> default_link_type.save() # We can also create a completely new link_type and add to the model - new_type = link_types.new('a') - new_type.link_type = 'Arterial' # Only ASCII letters and *_* allowed - # other fields are not mandatory + >>> new_type = link_types.new('a') + >>> new_type.link_type = 'Arterial' # Only ASCII letters and *_* allowed # other fields are not mandatory # We then save it to the database - new_type.save() + >>> new_type.save() # we can even keep editing and save it directly once we have added it to the project - new_type.lanes = 3 - new_type.lane_capacity = 1100 - new_type.save() + >>> new_type.lanes = 3 + >>> new_type.lane_capacity = 1100 + >>> new_type.save() """ @@ -89,7 +87,7 @@ def new(self, link_type_id: str) -> LinkType: return lt def delete(self, link_type_id: str) -> None: - """Removes the link_type with **link_type_id** from the project""" + """Removes the link_type with *link_type_id* from the project""" try: lt = self.__items[link_type_id] # type: LinkType lt.delete() @@ -101,13 +99,13 @@ def delete(self, link_type_id: str) -> None: self.logger.warning(f"Link type {link_type_id} was successfully removed from the project database") def get(self, link_type_id: str) -> LinkType: - """Get a link_type from the network by its **link_type_id**""" + """Get a link_type from the network by its *link_type_id*""" if link_type_id not in self.__items: raise ValueError(f"Link type {link_type_id} does not exist in the model") return self.__items[link_type_id] def get_by_name(self, link_type: str) -> LinkType: - """Get a link_type from the network by its **link_type** (i.e. name)""" + """Get a link_type from the network by its *link_type* (i.e. name)""" for lt in self.__items.values(): if lt.link_type.lower() == link_type.lower(): return lt diff --git a/aequilibrae/project/network/links.py b/aequilibrae/project/network/links.py index 8cf3d612c..127ce174c 100644 --- a/aequilibrae/project/network/links.py +++ b/aequilibrae/project/network/links.py @@ -13,20 +13,19 @@ class Links(BasicTable): """ Access to the API resources to manipulate the links table in the network - :: + .. code-block:: python - from aequilibrae import Project + >>> from aequilibrae import Project - proj = Project() - proj.open('path/to/project/folder') + >>> proj = Project.from_path("/tmp/test_project") - all_links = proj.network.links + >>> all_links = proj.network.links # We can just get one link in specific - link = all_links.get(4523) + >>> link = all_links.get(1) # We can save changes for all links we have edited so far - all_links.save() + >>> all_links.save() """ __max_id = -1 @@ -44,15 +43,15 @@ def __init__(self, net): self.refresh_fields() def get(self, link_id: int) -> Link: - """Get a link from the network by its **link_id** + """Get a link from the network by its *link_id* It raises an error if link_id does not exist - Args: - *link_id* (:obj:`int`): Id of a link to retrieve + :Arguments: + **link_id** (:obj:`int`): Id of a link to retrieve - Returns: - *link* (:obj:`Link`): Link object for requested link_id + :Returns: + **link** (:obj:`Link`): Link object for requested link_id """ link_id = int(link_id) if link_id in self.__items: @@ -68,8 +67,8 @@ def get(self, link_id: int) -> Link: def new(self) -> Link: """Creates a new link - Returns: - *link* (:obj:`Link`): A new link object populated only with link_id (not saved in the model yet) + :Returns: + **link** (:obj:`Link`): A new link object populated only with link_id (not saved in the model yet) """ data = {key: None for key in self.__fields} @@ -84,11 +83,11 @@ def copy_link(self, link_id: int) -> Link: It raises an error if link_id does not exist - Args: - *link_id* (:obj:`int`): Id of the link to copy + :Arguments: + **link_id** (:obj:`int`): Id of the link to copy - Returns: - *link* (:obj:`Link`): Link object for requested link_id + :Returns: + **link** (:obj:`Link`): Link object for requested link_id """ data = self.__link_data(int(link_id)) @@ -106,8 +105,8 @@ def copy_link(self, link_id: int) -> Link: def delete(self, link_id: int) -> None: """Removes the link with **link_id** from the project - Args: - *link_id* (:obj:`int`): Id of a link to delete""" + :Arguments: + **link_id** (:obj:`int`): Id of a link to delete""" d = 1 link_id = int(link_id) if link_id in self.__items: @@ -135,8 +134,8 @@ def refresh_fields(self) -> None: def data(self) -> pd.DataFrame: """Returns all links data as a Pandas dataFrame - Returns: - *table* (:obj:`DataFrame`): Pandas dataframe with all the links, complete with Geometry + :Returns: + **table** (:obj:`DataFrame`): Pandas dataframe with all the links, complete with Geometry """ dl = DataLoader(self.conn, "links") return dl.load_table() diff --git a/aequilibrae/project/network/modes.py b/aequilibrae/project/network/modes.py index bb6da6dc7..d5d98402d 100644 --- a/aequilibrae/project/network/modes.py +++ b/aequilibrae/project/network/modes.py @@ -7,49 +7,47 @@ class Modes: """ Access to the API resources to manipulate the modes table in the network - :: + .. code-block:: python - from aequilibrae import Project + >>> from aequilibrae import Project - p = Project() - p.open('path/to/project/folder') + >>> p = Project.from_path("/tmp/test_project") - modes = p.network.modes + >>> modes = p.network.modes # We can get a dictionary of all modes in the model - all_modes = modes.all_modes() + >>> all_modes = modes.all_modes() - #And do a bulk change and save it - for mode_id, mode_obj in all_modes.items(): - mode_obj.beta = 1 - mode_obj.save() + # And do a bulk change and save it + >>> for mode_id, mode_obj in all_modes.items(): + ... mode_obj.beta = 1 + ... mode_obj.save() # or just get one mode in specific - car_mode = modes.get('c') + >>> car_mode = modes.get('c') # or just get this same mode by name - car_mode = modes.get_by_name('c') + >>> car_mode = modes.get_by_name('car') # We can change the description of the mode - car_mode.description = 'personal autos only' + >>> car_mode.description = 'personal autos only' # Let's say we are using alpha to store the PCE for a future year with much smaller cars - car_mode.alpha = 0.95 + >>> car_mode.alpha = 0.95 # To save this mode we can simply - car_mode.save() + >>> car_mode.save() # We can also create a completely new mode and add to the model - new_mode = modes.new('k') - new_mode.mode_name = 'flying_car' # Only ASCII letters and *_* allowed - # other fields are not mandatory + >>> new_mode = modes.new('k') + >>> new_mode.mode_name = 'flying_car' # Only ASCII letters and *_* allowed # other fields are not mandatory # We then explicitly add it to the network - modes.add(new_mode) + >>> modes.add(new_mode) # we can even keep editing and save it directly once we have added it to the project - new_mode.description = 'this is my new description' - new_mode.save() + >>> new_mode.description = 'this is my new description' + >>> new_mode.save() """ def __init__(self, net): @@ -74,7 +72,7 @@ def add(self, mode: Mode) -> None: self.__update_list_of_modes() def delete(self, mode_id: str) -> None: - """Removes the mode with **mode_id** from the project""" + """Removes the mode with *mode_id* from the project""" try: self.curr.execute(f'delete from modes where mode_id="{mode_id}"') self.conn.commit() @@ -90,14 +88,14 @@ def fields(self) -> FieldEditor: return FieldEditor(self.project, "modes") def get(self, mode_id: str) -> Mode: - """Get a mode from the network by its **mode_id**""" + """Get a mode from the network by its *mode_id*""" self.__update_list_of_modes() if mode_id not in self.__all_modes: raise ValueError(f"Mode {mode_id} does not exist in the model") return Mode(mode_id, self.project) def get_by_name(self, mode: str) -> Mode: - """Get a mode from the network by its **mode_name**""" + """Get a mode from the network by its *mode_name*""" self.__update_list_of_modes() self.curr.execute(f"select mode_id from 'modes' where mode_name='{mode}'") found = self.curr.fetchone() diff --git a/aequilibrae/project/network/network.py b/aequilibrae/project/network/network.py index 2c0547c6b..094139545 100644 --- a/aequilibrae/project/network/network.py +++ b/aequilibrae/project/network/network.py @@ -62,7 +62,7 @@ def skimmable_fields(self): """ Returns a list of all fields that can be skimmed - Returns: + :Returns: :obj:`list`: List of all fields that can be skimmed """ curr = self.conn.cursor() @@ -112,7 +112,7 @@ def list_modes(self): """ Returns a list of all the modes in this model - Returns: + :Returns: :obj:`list`: List of all modes """ curr = self.conn.cursor() @@ -131,44 +131,43 @@ def create_from_osm( """ Downloads the network from Open-Street Maps - Args: - *west* (:obj:`float`, Optional): West most coordinate of the download bounding box + :Arguments: + **west** (:obj:`float`, Optional): West most coordinate of the download bounding box - *south* (:obj:`float`, Optional): South most coordinate of the download bounding box + **south** (:obj:`float`, Optional): South most coordinate of the download bounding box - *east* (:obj:`float`, Optional): East most coordinate of the download bounding box + **east** (:obj:`float`, Optional): East most coordinate of the download bounding box - *place_name* (:obj:`str`, Optional): If not downloading with East-West-North-South boundingbox, this is + **place_name** (:obj:`str`, Optional): If not downloading with East-West-North-South boundingbox, this is required - *modes* (:obj:`list`, Optional): List of all modes to be downloaded. Defaults to the modes in the parameter + **modes** (:obj:`list`, Optional): List of all modes to be downloaded. Defaults to the modes in the parameter file - p = Project() - p.new(nm) + .. code-block:: python - :: + >>> from aequilibrae import Project - from aequilibrae import Project, Parameters - p = Project() - p.new('path/to/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" + >>> 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 + >>> par.parameters['osm']['max_query_area_size'] = 10000000000 # And have no pause between successive queries - par.parameters['osm']['sleeptime'] = 0 + >>> par.parameters['osm']['sleeptime'] = 0 # Save the parameters to disk - par.write_back() + >>> par.write_back() - # And do the import - p.network.create_from_osm(place_name=my_beautiful_hometown) - p.close() + # Now we can import the network for any place we want + # p.network.create_from_osm(place_name="my_beautiful_hometown") + + >>> p.close() """ if self.count_links() > 0: @@ -253,18 +252,18 @@ def create_from_gmns( """ Creates AequilibraE model from links and nodes in GMNS format. - Args: - *link_file_path* (:obj:`str`): Path to a links csv file in GMNS format + :Arguments: + **link_file_path** (:obj:`str`): Path to a links csv file in GMNS format - *node_file_path* (:obj:`str`): Path to a nodes csv file in GMNS format + **node_file_path** (:obj:`str`): Path to a nodes csv file in GMNS format - *use_group_path* (:obj:`str`, Optional): Path to a csv table containing groupings of uses. This helps AequilibraE + **use_group_path** (:obj:`str`, Optional): Path to a csv table containing groupings of uses. This helps AequilibraE know when a GMNS use is actually a group of other GMNS uses - *geometry_path* (:obj:`str`, Optional): Path to a csv file containing geometry information for a line object, if not + **geometry_path** (:obj:`str`, Optional): Path to a csv file containing geometry information for a line object, if not specified in the link table - *srid* (:obj:`int`, Optional): Spatial Reference ID in which the GMNS geometries were created + **srid** (:obj:`int`, Optional): Spatial Reference ID in which the GMNS geometries were created """ gmns_builder = GMNSBuilder(self, link_file_path, node_file_path, use_group_path, geometry_path, srid) @@ -276,8 +275,8 @@ def export_to_gmns(self, path: str): """ Exports AequilibraE network to csv files in GMNS format. - Arg: - *path* (:obj:`str`): Output folder path. + :Arguments: + **path** (:obj:`str`): Output folder path. """ gmns_exporter = GMNSExporter(self, path) @@ -295,19 +294,21 @@ def build_graphs(self, fields: list = None, modes: list = None) -> None: When called, it overwrites all graphs previously created and stored in the networks' dictionary of graphs - Args: - *fields* (:obj:`list`, optional): When working with very large graphs with large number of fields in the + :Arguments: + **fields** (:obj:`list`, optional): When working with very large graphs with large number of fields in the database, it may be useful to specify which fields to use - *modes* (:obj:`list`, optional): When working with very large graphs with large number of fields in the + **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 - :: - p = Project() - p.open(nm) - fields = ['distance'] - p.network.build_graphs(fields, modes = ['c', 'w']) + .. code-block:: python + + >>> from aequilibrae import Project + + >>> p = Project.from_path("/tmp/test_project") + >>> fields = ['distance'] + >>> p.network.build_graphs(fields, modes = ['c', 'w']) """ from aequilibrae.paths import Graph @@ -355,8 +356,8 @@ def set_time_field(self, time_field: str) -> None: """ Set the time field for all graphs built in the model - Args: - *time_field* (:obj:`str`): Network field with travel time information + :Arguments: + **time_field** (:obj:`str`): Network field with travel time information """ for m, g in self.graphs.items(): if time_field not in list(g.graph.columns): @@ -369,7 +370,7 @@ def count_links(self) -> int: """ Returns the number of links in the model - Returns: + :Returns: :obj:`int`: Number of links """ return self.__count_items("link_id", "links", "link_id>=0") @@ -378,7 +379,7 @@ def count_centroids(self) -> int: """ Returns the number of centroids in the model - Returns: + :Returns: :obj:`int`: Number of centroids """ return self.__count_items("node_id", "nodes", "is_centroid=1") @@ -387,7 +388,7 @@ def count_nodes(self) -> int: """ Returns the number of nodes in the model - Returns: + :Returns: :obj:`int`: Number of nodes """ return self.__count_items("node_id", "nodes", "node_id>=0") @@ -395,8 +396,8 @@ def count_nodes(self) -> int: def extent(self): """Queries the extent of the network included in the model - Returns: - *model extent* (:obj:`Polygon`): Shapely polygon with the bounding box of the model network. + :Returns: + **model extent** (:obj:`Polygon`): Shapely polygon with the bounding box of the model network. """ curr = self.conn.cursor() curr.execute('Select ST_asBinary(GetLayerExtent("Links"))') @@ -406,8 +407,8 @@ def extent(self): def convex_hull(self) -> Polygon: """Queries the model for the convex hull of the entire network - Returns: - *model coverage* (:obj:`Polygon`): Shapely (Multi)polygon of the model network. + :Returns: + **model coverage** (:obj:`Polygon`): Shapely (Multi)polygon of the model network. """ curr = self.conn.cursor() curr.execute('Select ST_asBinary("geometry") from Links where ST_Length("geometry") > 0;') diff --git a/aequilibrae/project/network/node.py b/aequilibrae/project/network/node.py index b5670f759..dce36168f 100644 --- a/aequilibrae/project/network/node.py +++ b/aequilibrae/project/network/node.py @@ -6,32 +6,28 @@ class Node(SafeClass): """A Node object represents a single record in the *nodes* table - :: + .. code-block:: python - from aequilibrae import Project - from shapely.geometry import Point + >>> from aequilibrae import Project + >>> from shapely.geometry import Point - proj = Project() - proj.open('path/to/project/folder') + >>> proj = Project.from_path("/tmp/test_project") - all_nodes = proj.network.nodes + >>> all_nodes = proj.network.nodes # We can just get one link in specific - node1 = all_nodes.get(7890) + >>> node1 = all_nodes.get(7) # We can find out which fields exist for the links - which_fields_do_we_have = node1.data_fields() - - # And edit each one like this - node1.comment = 'This node is important' + >>> which_fields_do_we_have = node1.data_fields() # It success if the node_id already does not exist - node1.renumber(998877) + >>> node1.renumber(998877) - node.geometry = Point(1,2) + >>> node1.geometry = Point(1,2) # We can just save the node - node1.save() + >>> node1.save() """ def __init__(self, dataset, project): @@ -62,8 +58,8 @@ def save(self): def data_fields(self) -> list: """lists all data fields for the node, as available in the database - Returns: - *data fields* (:obj:`list`): list of all fields available for editing + :Returns: + **data fields** (:obj:`list`): list of all fields available for editing """ return list(self.__original__.keys()) @@ -73,8 +69,8 @@ def renumber(self, new_id: int): Logs a warning if another node already exists with this node_id - Args: - *new_id* (:obj:`int`): New node_id + :Arguments: + **new_id** (:obj:`int`): New node_id """ new_id = int(new_id) @@ -130,16 +126,15 @@ def connect_mode(self, area: Polygon, mode_id: str, link_types="", connectors=1) If fewer candidates than required connectors are found, all candidates are connected. - Args: - - *area* (:obj:`Polygon`): Initial area where AequilibraE will look for nodes to connect + :Arguments: + **area** (:obj:`Polygon`): Initial area where AequilibraE will look for nodes to connect - *mode_id* (:obj:`str`): Mode ID we are trying to connect + **mode_id** (:obj:`str`): Mode ID we are trying to connect - *link_types* (:obj:`str`, `Optional`): String with all the link type IDs that can be considered. - eg: yCdR. Defaults to ALL link types + **link_types** (:obj:`str`, `Optional`): String with all the link type IDs that can + be considered. eg: yCdR. Defaults to ALL link types - *connectors* (:obj:`int`, `Optional`): Number of connectors to add. Defaults to 1 + **connectors** (:obj:`int`, `Optional`): Number of connectors to add. Defaults to 1 """ if self.is_centroid != 1 or self.__original__["is_centroid"] != 1: self._logger.warning("Connecting a mode only makes sense for centroids and not for regular nodes") diff --git a/aequilibrae/project/network/nodes.py b/aequilibrae/project/network/nodes.py index d242a7baf..fb0c5eac9 100644 --- a/aequilibrae/project/network/nodes.py +++ b/aequilibrae/project/network/nodes.py @@ -12,20 +12,19 @@ class Nodes(BasicTable): """ Access to the API resources to manipulate the links table in the network - :: + .. code-block:: python - from aequilibrae import Project + >>> from aequilibrae import Project - proj = Project() - proj.open('path/to/project/folder') + >>> proj = Project.from_path("/tmp/test_project") - all_nodes = proj.network.nodes + >>> all_nodes = proj.network.nodes # We can just get one link in specific - node = all_nodes.get(7894) + >>> node = all_nodes.get(21) # We can save changes for all nodes we have edited so far - all_nodes.save() + >>> all_nodes.save() """ #: Query sql for retrieving nodes @@ -45,11 +44,11 @@ def get(self, node_id: int) -> Node: It raises an error if node_id does not exist - Args: - *node_id* (:obj:`int`): Id of a node to retrieve + :Arguments: + **node_id** (:obj:`int`): Id of a node to retrieve - Returns: - *node* (:obj:`Node`): Node object for requested node_id + :Returns: + **node** (:obj:`Node`): Node object for requested node_id """ if node_id in self.__items: @@ -88,8 +87,8 @@ def refresh(self): def new_centroid(self, node_id: int) -> Node: """Creates a new centroid with a given ID - Args: - *node_id* (:obj:`int`): Id of the centroid to be created + :Arguments: + **node_id** (:obj:`int`): Id of the centroid to be created """ self._curr.execute("select count(*) from nodes where node_id=?", [node_id]) @@ -109,10 +108,10 @@ def save(self): @property def data(self) -> pd.DataFrame: - """Returns all nodes data as a Pandas dataFrame + """Returns all nodes data as a Pandas DataFrame - Returns: - *table* (:obj:`DataFrame`): Pandas dataframe with all the nodes, complete with Geometry + :Returns: + **table** (:obj:`DataFrame`): Pandas DataFrame with all the nodes, complete with Geometry """ dl = DataLoader(self.conn, "nodes") return dl.load_table() diff --git a/aequilibrae/project/network/osm_downloader.py b/aequilibrae/project/network/osm_downloader.py index b2ecc3ab7..f0794f71a 100644 --- a/aequilibrae/project/network/osm_downloader.py +++ b/aequilibrae/project/network/osm_downloader.py @@ -82,25 +82,17 @@ def doWork(self): self.__emit_all(["FinishedDownloading", 0]) def overpass_request(self, data, pause_duration=None, timeout=180, error_pause_duration=None): - """ - Send a request to the Overpass API via HTTP POST and return the JSON - response. - - Parameters - ---------- - data : dict or OrderedDict - key-value pairs of parameters to post to the API - pause_duration : int - how long to pause in seconds before requests, if None, will query API + """Send a request to the Overpass API via HTTP POST and return the JSON response. + + :Arguments: + **data**(:obj:`dict` or `OrderedDict`): key-value pairs of parameters to post to the API + **pause_duration** (:obj:`int`): how long to pause in seconds before requests, if None, will query API status endpoint to find when next slot is available - timeout : int - the timeout interval for the requests library - error_pause_duration : int - how long to pause in seconds before re-trying requests if error - - Returns - ------- - dict + **timeout** (:obj:`int`): the timeout interval for the requests library + **error_pause_duration**(:obj:`int`): how long to pause in seconds before re-trying requests if error + + :Returns: + :obj:`dict` """ # define the Overpass API URL, then construct a GET-style URL as a string to diff --git a/aequilibrae/project/project.py b/aequilibrae/project/project.py index a117d0d5d..bb464c2e9 100644 --- a/aequilibrae/project/project.py +++ b/aequilibrae/project/project.py @@ -15,23 +15,36 @@ from aequilibrae.project.zoning import Zoning from aequilibrae.reference_files import spatialite_database from aequilibrae.log import get_log_handler -from .project_cleaning import clean -from .project_creation import initialize_tables -from ..transit import Transit +from aequilibrae.project.project_cleaning import clean +from aequilibrae.project.project_creation import initialize_tables +from aequilibrae.transit.transit import Transit class Project: """AequilibraE project class - :: + .. code-block:: python + :caption: Create Project - from aequilibrae.project import Project + >>> newfile = Project() + >>> newfile.new('/tmp/new_project') - existing = Project() - existing.load('path/to/existing/project/folder') + .. 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 - newfile = Project() - newfile.new('path/to/new/project/folder') """ def __init__(self): @@ -44,12 +57,18 @@ def __init__(self): self.logger: logging.Logger = None self.transit: Transit = None + @classmethod + def from_path(cls, project_folder): + project = Project() + project.open(project_folder) + return project + def open(self, project_path: str) -> None: """ Loads project from disk - Args: - *project_path* (:obj:`str`): Full path to the project data folder. If the project inside does + :Arguments: + **project_path** (:obj:`str`): Full path to the project data folder. If the project inside does not exist, it will fail. """ @@ -71,8 +90,8 @@ def open(self, project_path: str) -> None: def new(self, project_path: str) -> None: """Creates a new project - Args: - *project_path* (:obj:`str`): Full path to the project data folder. If folder exists, it will fail + :Arguments: + **project_path** (:obj:`str`): Full path to the project data folder. If folder exists, it will fail """ self.project_base_path = project_path @@ -121,8 +140,11 @@ def load(self, project_path: str) -> None: """ Loads project from disk - Args: - *project_path* (:obj:`str`): Full path to the project data folder. If the project inside does + .. deprecated:: 0.7.0 + Use :func:`open` instead. + + :Arguments: + **project_path** (:obj:`str`): Full path to the project data folder. If the project inside does not exist, it will fail. """ warnings.warn(f"Function has been deprecated. Use my_project.open({project_path}) instead", DeprecationWarning) diff --git a/aequilibrae/project/zone.py b/aequilibrae/project/zone.py index 0c5870c28..b16d55cb8 100644 --- a/aequilibrae/project/zone.py +++ b/aequilibrae/project/zone.py @@ -58,10 +58,10 @@ def save(self): def add_centroid(self, point: Point, robust=True) -> None: """Adds a centroid to the network file - Args: - *point* (:obj:`Point`): Shapely Point corresponding to the desired centroid position. + :Arguments: + **point** (:obj:`Point`): Shapely Point corresponding to the desired centroid position. If None, uses the geometric center of the zone - *robust* (:obj:`Bool`, Optional): Moves the centroid location around to avoid node conflict. + **robust** (:obj:`Bool`, Optional): Moves the centroid location around to avoid node conflict. Defaults to True. """ @@ -112,13 +112,13 @@ def connect_mode(self, mode_id: str, link_types="", connectors=1) -> None: If fewer candidates than required connectors are found, all candidates are connected. - Args: - *mode_id* (:obj:`str`): Mode ID we are trying to connect + :Arguments: + **mode_id** (:obj:`str`): Mode ID we are trying to connect - *link_types* (:obj:`str`, `Optional`): String with all the link type IDs that can be considered. - eg: yCdR. Defaults to ALL link types + **link_types** (:obj:`str`, `Optional`): String with all the link type IDs that can be considered. + eg: yCdR. Defaults to ALL link types - *connectors* (:obj:`int`, `Optional`): Number of connectors to add. Defaults to 1 + **connectors** (:obj:`int`, `Optional`): Number of connectors to add. Defaults to 1 """ connector_creation( self.geometry, @@ -133,8 +133,8 @@ def connect_mode(self, mode_id: str, link_types="", connectors=1) -> None: def disconnect_mode(self, mode_id: str) -> None: """Removes centroid connectors for the desired mode from the network file - Args: - *mode_id* (:obj:`str`): Mode ID we are trying to disconnect from this zone + :Arguments: + **mode_id** (:obj:`str`): Mode ID we are trying to disconnect from this zone """ curr = self.conn.cursor() diff --git a/aequilibrae/project/zoning.py b/aequilibrae/project/zoning.py index 3143ca9cc..5f519dedd 100644 --- a/aequilibrae/project/zoning.py +++ b/aequilibrae/project/zoning.py @@ -6,37 +6,38 @@ from shapely.geometry import Point, Polygon, LineString, MultiLineString from shapely.ops import unary_union +from aequilibrae.project.basic_table import BasicTable from aequilibrae.project.project_creation import run_queries_from_sql_file from aequilibrae.project.table_loader import TableLoader from aequilibrae.utils.geo_index import GeoIndex -from .basic_table import BasicTable -from .zone import Zone +from aequilibrae.project.zone import Zone class Zoning(BasicTable): """ Access to the API resources to manipulate the zones table in the project - :: + .. code-block:: python - from aequilibrae import Project + >>> from aequilibrae import Project - p = Project() - p.open('path/to/project/folder') + >>> project = Project.from_path("/tmp/test_project") - zones = p.zoning - # We edit the fields for a particular zone - zone_downtown = zones.get(1) - zone_downtown.population = 637 - zone_downtown.employment = 10039 - zone_downtown.save() + >>> zoning = project.zoning - fields = zones.fields + >>> zone_downtown = zoning.get(1) + >>> zone_downtown.population = 637 + >>> zone_downtown.employment = 10039 + >>> zone_downtown.save() - # We can also add one more field to the table - fields.add('parking_spots', 'Total licensed parking spots', 'INTEGER') + # changing the value for an existing value/field + >>> project.about.scenario_name = 'Just a better scenario name' + >>> project.about.write_back() + # We can also add one more field to the table + >>> fields = zoning.fields + >>> fields.add('parking_spots', 'Total licensed parking spots', 'INTEGER') """ def __init__(self, network): @@ -52,8 +53,8 @@ def __init__(self, network): def new(self, zone_id: int) -> Zone: """Creates a new zone - Returns: - *zone* (:obj:`Zone`): A new zone object populated only with zone_id (but not saved in the model yet) + :Returns: + **zone** (:obj:`Zone`): A new zone object populated only with zone_id (but not saved in the model yet) """ if zone_id in self.__items: @@ -78,8 +79,8 @@ def create_zoning_layer(self): def coverage(self) -> Polygon: """Returns a single polygon for the entire zoning coverage - Returns: - *model coverage* (:obj:`Polygon`): Shapely (Multi)polygon of the zoning system. + :Returns: + **model coverage** (:obj:`Polygon`): Shapely (Multi)polygon of the zoning system. """ self._curr.execute('Select ST_asBinary("geometry") from zones;') polygons = [shapely.wkb.loads(x[0]) for x in self._curr.fetchall()] @@ -105,11 +106,11 @@ def get_closest_zone(self, geometry: Union[Point, LineString, MultiLineString]) If the geometry is not fully enclosed by any zone, the zone closest to the geometry is returned - Args: - *geometry* (:obj:`Point` or :obj:`LineString`): A Shapely geometry object + :Arguments: + **geometry** (:obj:`Point` or :obj:`LineString`): A Shapely geometry object - Return: - *zone_id* (:obj:`int`): ID of the zone applicable to the point provided + :Return: + **zone_id** (:obj:`int`): ID of the zone applicable to the point provided """ nearest = self.__geo_index.nearest(geometry, 10) diff --git a/aequilibrae/transit/functions/compute_line_bearing.py b/aequilibrae/transit/functions/compute_line_bearing.py index c9e60c835..d77bb5cf8 100644 --- a/aequilibrae/transit/functions/compute_line_bearing.py +++ b/aequilibrae/transit/functions/compute_line_bearing.py @@ -6,9 +6,9 @@ def compute_line_bearing(point_a: tuple, point_b: tuple) -> float: Computes line bearing for projected (cartesian) coordinates. For non-projected coordinates, see: https://gist.github.com/jeromer/2005586 - Args: - *point_a* (:obj:`tuple`): first point coordinates (lat, lon) - *point_b* (:obj:`tuple`): second point coordinates (lat, lon) + :Arguments: + **point_a** (:obj:`tuple`): first point coordinates (lat, lon) + **point_b** (:obj:`tuple`): second point coordinates (lat, lon) """ delta_lat = abs(point_a[1] - point_b[1]) diff --git a/aequilibrae/transit/functions/del_pattern.py b/aequilibrae/transit/functions/del_pattern.py index a7bc85a03..181a77277 100644 --- a/aequilibrae/transit/functions/del_pattern.py +++ b/aequilibrae/transit/functions/del_pattern.py @@ -5,8 +5,8 @@ def delete_pattern(pattern_id: int): """Deletes all information regarding one specific transit_pattern. - Args: - *pattern_id* (:obj:`str`): pattern_id as present in the database + :Arguments: + **pattern_id** (:obj:`str`): pattern_id as present in the database """ sqls = [ """DELETE from trips where trip_id IN diff --git a/aequilibrae/transit/functions/path_storage.py b/aequilibrae/transit/functions/path_storage.py index 98af6aeb8..cf649dfbd 100644 --- a/aequilibrae/transit/functions/path_storage.py +++ b/aequilibrae/transit/functions/path_storage.py @@ -7,7 +7,7 @@ class PathStorage: preserves the entire shortest path tree when computing a path between two nodes and can re-trace the same tree for a path from the same origin to a different destination. - Since this caching in memory can take too much memory, the **threshold** parameter exists to limit the number + Since this caching in memory can take too much memory, the *threshold* parameter exists to limit the number of path objects kept in memory. If you have a large amount of memory in your system, you can set the threshold class variable accordingly. diff --git a/aequilibrae/transit/gtfs_loader.py b/aequilibrae/transit/gtfs_loader.py index c881b2815..3fb4ad0fb 100644 --- a/aequilibrae/transit/gtfs_loader.py +++ b/aequilibrae/transit/gtfs_loader.py @@ -63,8 +63,9 @@ def __init__(self): def set_feed_path(self, file_path): """Sets GTFS feed source to be used - Args: - *file_path* (:obj:`str`): Full path to the GTFS feed (e.g. 'D:/project/my_gtfs_feed.zip') + + :Arguments: + **file_path** (:obj:`str`): Full path to the GTFS feed (e.g. 'D:/project/my_gtfs_feed.zip') """ self.archive_dir = file_path @@ -88,8 +89,8 @@ def _set_maximum_speeds(self, max_speeds: dict): def load_data(self, service_date: str): """Loads the data for a respective service date. - Args.: - *service_date*(:obj:`str`): service date. e.g. "2020-04-01". + :Arguments: + **service_date** (:obj:`str`): service date. e.g. "2020-04-01". """ ag_id = self.agency.agency self.logger.info(f"Loading data for {service_date} from the {ag_id} GTFS feed. This may take some time") diff --git a/aequilibrae/transit/lib_gtfs.py b/aequilibrae/transit/lib_gtfs.py index 3e6b10525..4b247ff02 100644 --- a/aequilibrae/transit/lib_gtfs.py +++ b/aequilibrae/transit/lib_gtfs.py @@ -37,13 +37,13 @@ class GTFSRouteSystemBuilder(WorkerThread): def __init__(self, network, agency_identifier, file_path, day="", description="", default_capacities={}): """Instantiates a transit class for the network - Args: + :Arguments: - *local network* (:obj:`Network`): Supply model to which this GTFS will be imported - *agency_identifier* (:obj:`str`): ID for the agency this feed refers to (e.g. 'CTA') - *file_path* (:obj:`str`): Full path to the GTFS feed (e.g. 'D:/project/my_gtfs_feed.zip') - *day* (:obj:`str`, *Optional*): Service data contained in this field to be imported (e.g. '2019-10-04') - *description* (:obj:`str`, *Optional*): Description for this feed (e.g. 'CTA19 fixed by John after coffee') + **local network** (:obj:`Network`): Supply model to which this GTFS will be imported + **agency_identifier** (:obj:`str`): ID for the agency this feed refers to (e.g. 'CTA') + **file_path** (:obj:`str`): Full path to the GTFS feed (e.g. 'D:/project/my_gtfs_feed.zip') + **day** (:obj:`str`, *Optional*): Service data contained in this field to be imported (e.g. '2019-10-04') + **description** (:obj:`str`, *Optional*): Description for this feed (e.g. 'CTA19 fixed by John after coffee') """ WorkerThread.__init__(self, None) @@ -86,8 +86,8 @@ def __init__(self, network, agency_identifier, file_path, day="", description="" def set_capacities(self, capacities: dict): """Sets default capacities for modes/vehicles. - Args: - *capacities* (:obj:`dict`): Dictionary with GTFS types as keys, each with a list + :Arguments: + **capacities** (:obj:`dict`): Dictionary with GTFS types as keys, each with a list of 3 items for values for capacities: seated and total i.e. -> "{0: [150, 300],...}" """ @@ -96,8 +96,8 @@ def set_capacities(self, capacities: dict): def set_maximum_speeds(self, max_speeds: pd.DataFrame): """Sets the maximum speeds to be enforced at segments. - Args: - *max_speeds* (:obj:`pd.DataFrame`): Requires 4 fields: mode, min_distance, max_distance, speed. + :Arguments: + **max_speeds** (:obj:`pd.DataFrame`): Requires 4 fields: mode, min_distance, max_distance, speed. Modes not covered in the data will not be touched and distance brackets not covered will receive the maximum speed, with a warning """ @@ -107,16 +107,16 @@ def set_maximum_speeds(self, max_speeds: pd.DataFrame): def dates_available(self) -> list: """Returns a list of all dates available for this feed. - Returns: - *feed dates* (:obj:`list`): list of all dates available for this feed + :Returns: + **feed dates** (:obj:`list`): list of all dates available for this feed """ return deepcopy(self.gtfs_data.feed_dates) def set_allow_map_match(self, allow=True): """Changes behavior for finding transit-link shapes. Defaults to True. - Args: - *allow* (:obj:`bool` *optional*): If True, allows uses map-matching in search of precise + :Arguments: + **allow** (:obj:`bool` *optional*): If True, allows uses map-matching in search of precise transit_link shapes. If False, sets transit_link shapes equal to straight lines between stops. In the presence of GTFS raw shapes it has no effect. """ @@ -130,8 +130,8 @@ def map_match(self, route_types=[3]) -> None: For a reference of route types, see https://developers.google.com/transit/gtfs/reference#routestxt - Args: - *route_types* (:obj:`List[int]` or :obj:`Tuple[int]`): Default is [3], for bus only + :Arguments: + **route_types** (:obj:`List[int]` or :obj:`Tuple[int]`): Default is [3], for bus only """ if not isinstance(route_types, list) and not isinstance(route_types, tuple): raise TypeError("Route_types must be list or tuple") @@ -153,16 +153,16 @@ def map_match(self, route_types=[3]) -> None: def set_agency_identifier(self, agency_id: str) -> None: """Adds agency ID to this GTFS for use on import. - Args: - *agency_id* (:obj:`str`): ID for the agency this feed refers to (e.g. 'CTA') + :Arguments: + **agency_id** (:obj:`str`): ID for the agency this feed refers to (e.g. 'CTA') """ self.gtfs_data.agency.agency = agency_id def set_feed(self, feed_path: str) -> None: """Sets GTFS feed source to be used. - Args: - *file_path* (:obj:`str`): Full path to the GTFS feed (e.g. 'D:/project/my_gtfs_feed.zip') + :Arguments: + **file_path** (:obj:`str`): Full path to the GTFS feed (e.g. 'D:/project/my_gtfs_feed.zip') """ self.gtfs_data.set_feed_path(feed_path) self.gtfs_data.agency.feed_date = self.gtfs_data.feed_date @@ -170,8 +170,8 @@ def set_feed(self, feed_path: str) -> None: def set_description(self, description: str) -> None: """Adds description to be added to the imported layers metadata - Args: - *description* (:obj:`str`): Description for this feed (e.g. 'CTA2019 fixed by John Doe after strong coffee') + :Arguments: + **description** (:obj:`str`): Description for this feed (e.g. 'CTA2019 fixed by John Doe after strong coffee') """ self.description = description @@ -182,8 +182,8 @@ def set_date(self, service_date: str) -> None: def load_date(self, service_date: str) -> None: """Loads the transit services available for *service_date* - Args: - *service_date* (:obj:`str`): Service data contained in this field to be imported (e.g. '2019-10-04') + :Arguments: + **service_date** (:obj:`str`): Service data contained in this field to be imported (e.g. '2019-10-04') """ if self.srid is None: raise ValueError("We cannot load data without an SRID") @@ -440,8 +440,8 @@ def _get_fare_attributes_by_date(self, service_date: str) -> dict: def builds_link_graphs_with_broken_stops(self): """Build the graph for links for a certain mode while splitting the closest links at stops' projection - Args: - *mode_id* (:obj:`int`): Mode ID for which we will build the graph for + :Arguments: + **mode_id** (:obj:`int`): Mode ID for which we will build the graph for """ route_types = list(set([r.route_type for r in self.select_routes.values()])) diff --git a/aequilibrae/transit/map_matching_graph.py b/aequilibrae/transit/map_matching_graph.py index 846ca3c0f..69ee5a099 100644 --- a/aequilibrae/transit/map_matching_graph.py +++ b/aequilibrae/transit/map_matching_graph.py @@ -62,9 +62,9 @@ def __init__(self, lib_gtfs, mtmm): def build_graph_with_broken_stops(self, mode_id: int, distance_to_project=200): """Build the graph for links for a certain mode while splitting the closest links at stops' projection - Args: - *mode_id* (:obj:`int`): Mode ID for which we will build the graph for - *distance_to_project* (:obj:`float`, **Optional**): Radius search for links to break at the stops. Defaults to 50m + :Arguments: + **mode_id** (:obj:`int`): Mode ID for which we will build the graph for + **distance_to_project** (:obj:`float`, `Optional`): Radius search for links to break at the stops. Defaults to 50m """ self.logger.debug(f"Called build_graph_with_broken_stops for mode_id={mode_id}") self.mode_id = mode_id diff --git a/aequilibrae/transit/transit.py b/aequilibrae/transit/transit.py index a3519f022..7da7d1994 100644 --- a/aequilibrae/transit/transit.py +++ b/aequilibrae/transit/transit.py @@ -23,8 +23,9 @@ class Transit: def __init__(self, project): """ - Args.: - *project* (:obj:``): + :Arguments: + **project** (:obj:`Project`, optional): The Project to connect to. By default, uses the currently + active project """ self.project_base_path = project.project_base_path @@ -36,14 +37,17 @@ def __init__(self, project): def new_gtfs_builder(self, agency, file_path, day="", description="") -> GTFSRouteSystemBuilder: """Returns a GTFSRouteSystemBuilder object compatible with the project - Args: - *agency* (:obj:`str`): Name for the agency this feed refers to (e.g. 'CTA') - *file_path* (:obj:`str`): Full path to the GTFS feed (e.g. 'D:/project/my_gtfs_feed.zip') - *day* (:obj:`str`, *Optional*): Service data contained in this field to be imported (e.g. '2019-10-04') - *description* (:obj:`str`, *Optional*): Description for this feed (e.g. 'CTA2019 fixed by John Doe') + :Arguments: + **agency** (:obj:`str`): Name for the agency this feed refers to (e.g. 'CTA') - Return: - *gtfs_feed* (:obj:`StaticGTFS`): A GTFS feed that can be added to this network + **file_path** (:obj:`str`): Full path to the GTFS feed (e.g. 'D:/project/my_gtfs_feed.zip') + + **day** (:obj:`str`, *Optional*): Service data contained in this field to be imported (e.g. '2019-10-04') + + **description** (:obj:`str`, *Optional*): Description for this feed (e.g. 'CTA2019 fixed by John Doe') + + :Return: + **gtfs_feed** (:obj:`StaticGTFS`): A GTFS feed that can be added to this network """ gtfs = GTFSRouteSystemBuilder( network=self.project_base_path, diff --git a/aequilibrae/utils/create_delaunay_network.py b/aequilibrae/utils/create_delaunay_network.py index ca1420403..2b59d5ada 100644 --- a/aequilibrae/utils/create_delaunay_network.py +++ b/aequilibrae/utils/create_delaunay_network.py @@ -17,8 +17,8 @@ class DelaunayAnalysis: def __init__(self, project): """Start a Delaunay analysis - Args: - project (:obj:`Project`): The Project to connect to + :Arguments: + **project** (:obj:`Project`): The Project to connect to """ self.project = project @@ -27,10 +27,10 @@ def __init__(self, project): def create_network(self, source="zones", overwrite=False): """Creates a delaunay network based on the existing model - Args: - source (:obj:`str`, optional): Source of the centroids/zones. Either ``zones`` or ``network``. Default ``zones`` + :Arguments: + **source** (:obj:`str`, optional): Source of the centroids/zones. Either ``zones`` or ``network``. Default ``zones`` - overwrite path (:obj:`bool`, optional): Whether to should overwrite an existing Delaunay Network. Default ``False`` + **overwrite path** (:obj:`bool`, optional): Whether to should overwrite an existing Delaunay Network. Default ``False`` """ diff --git a/aequilibrae/utils/create_example.py b/aequilibrae/utils/create_example.py index eeb49ce6f..d7b6b7e0c 100644 --- a/aequilibrae/utils/create_example.py +++ b/aequilibrae/utils/create_example.py @@ -7,12 +7,12 @@ def create_example(path: str, from_model="sioux_falls") -> Project: """Copies an example model to a new project project and returns the project handle - Args: - *path* (:obj:`str`): Path where to create a new model. must be a non-existing folder/directory. - *from_model path* (:obj:`str`, `Optional`): Example to create from *sioux_falls*, *nauru* or *coquimbo*. Defaults to - *sioux_falls* - Returns: - *project* (:obj:`Project`): Aequilibrae Project handle (open) + :Arguments: + **path** (:obj:`str`): Path where to create a new model. must be a non-existing folder/directory. + **from_model** (:obj:`str`, `Optional`): Example to create from *sioux_falls*, *nauru* or *coquimbo*. + Defaults to *sioux_falls* + :Returns: + **project** (:obj:`Project`): Aequilibrae Project handle (open) """ if os.path.isdir(path): diff --git a/aequilibrae/utils/db_utils.py b/aequilibrae/utils/db_utils.py index 669b9e3c8..d1bee1fb8 100644 --- a/aequilibrae/utils/db_utils.py +++ b/aequilibrae/utils/db_utils.py @@ -30,9 +30,13 @@ class commit_and_close: def __init__(self, db: Union[str, Path, Connection], commit: bool = True, missing_ok: bool = False): """ - :param db: The database (filename or connection) to be managed - :param commit: Boolean indicating if a commit/rollback should be attempted on closing - :param missing_ok: Boolean indicating that the db is not expected to exist yet + :Arguments: + + **db** (:obj:`Union[str, Path, Connection]`): The database (filename or connection) to be managed + + **commit** (:obj:`bool`): Boolean indicating if a commit/rollback should be attempted on closing + + **missing_ok** (:obj:`bool`): Boolean indicating that the db is not expected to exist yet """ if isinstance(db, str) or isinstance(db, Path): db = safe_connect(db, missing_ok) diff --git a/aequilibrae/utils/geo_index.py b/aequilibrae/utils/geo_index.py index e7ab9afad..24a3fd1c5 100644 --- a/aequilibrae/utils/geo_index.py +++ b/aequilibrae/utils/geo_index.py @@ -39,9 +39,9 @@ def insert( ) -> None: """Inserts a valid shapely geometry in the index - Args: - *feature_id* (:obj:`int`): ID of the geometry being inserted - *geo* (:obj:`Shapely geometry`): Any valid shapely geometry + :Arguments: + **feature_id** (:obj:`int`): ID of the geometry being inserted + **geo** (:obj:`Shapely.geometry`): Any valid shapely geometry """ self.built = True if env == "QGIS": @@ -59,11 +59,11 @@ def insert( def nearest(self, geo: Union[Point, Polygon, LineString, MultiPoint, MultiPolygon], num_results) -> List[int]: """Finds nearest neighbor for a given geometry - Args: - *geo* (:obj:`Shapely geometry`): Any valid shapely geometry - *num_results* (:obj:`int`): A positive integer for the number of neighbors to return - Return: - *neighbors* (:obj:`List[int]`): List of IDs of the closest neighbors in the index + :Arguments: + **geo** (:obj:`Shapely geometry`): Any valid shapely geometry + **num_results** (:obj:`int`): A positive integer for the number of neighbors to return + :Return: + **neighbors** (:obj:`List[int]`): List of IDs of the closest neighbors in the index """ if env == "QGIS": g = QgsGeometry() diff --git a/aequilibrae/utils/get_table.py b/aequilibrae/utils/get_table.py index 434b3601e..0ba214a0d 100644 --- a/aequilibrae/utils/get_table.py +++ b/aequilibrae/utils/get_table.py @@ -5,9 +5,9 @@ def get_table(table_name, conn): """ Selects table from database. - Args: - *table_name* (:obj:`str`): desired table name - *conn* (:obj:`sqlite3.Connection`): database connection + :Arguments: + **table_name** (:obj:`str`): desired table name + **conn** (:obj:`sqlite3.Connection`): database connection """ return pd.read_sql(f"SELECT * FROM {table_name};", con=conn) diff --git a/aequilibrae/utils/list_tables_in_db.py b/aequilibrae/utils/list_tables_in_db.py index f78670c48..d65dc9e47 100644 --- a/aequilibrae/utils/list_tables_in_db.py +++ b/aequilibrae/utils/list_tables_in_db.py @@ -2,7 +2,7 @@ def list_tables_in_db(conn): """ Return a list with all tables within a database. - Args: - *conn* (:obj: `sqlite3.Connection`): database connection + :Arguments: + **conn** (:obj: `sqlite3.Connection`): database connection """ return [x[0] for x in conn.execute("SELECT name FROM sqlite_master WHERE type ='table'").fetchall()] diff --git a/docs/create_docs_data.py b/docs/create_docs_data.py new file mode 100644 index 000000000..d94e41467 --- /dev/null +++ b/docs/create_docs_data.py @@ -0,0 +1,17 @@ +import sys +from pathlib import Path + +project_dir = Path(__file__).parent.parent +if str(project_dir) not in sys.path: + sys.path.append(str(project_dir)) + +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() \ No newline at end of file diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 84de5b094..b99b3f642 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -5,11 +5,15 @@ pyshp cython enum34>=1.1.6 Sphinx -sphinx_theme +pydata-sphinx-theme sphinx_autodoc_annotation nbsphinx pillow matplotlib folium keplergl -sphinx-git \ No newline at end of file +sphinx-git +sphinx-gallery +sphinx-panels +sphinx_theme +sphinx-copybutton \ No newline at end of file diff --git a/docs/source/_static/switcher.json b/docs/source/_static/switcher.json new file mode 100644 index 000000000..1493b0c36 --- /dev/null +++ b/docs/source/_static/switcher.json @@ -0,0 +1,102 @@ +[ + { + "name": "0.9.0", + "version": "0.9.0", + "url": "https://aequilibrae.com/python/V.0.9.0/" + }, + { + "name": "0.8.3", + "version": "0.8.3", + "url": "https://aequilibrae.com/python/V.0.8.3/" + }, + { + "name": "0.8.2", + "version": "0.8.2", + "url": "https://aequilibrae.com/python/V.0.8.2/" + }, + { + "name": "0.8.1", + "version": "0.8.1", + "url": "https://aequilibrae.com/python/V.0.8.1/" + }, + { + "name": "0.8.0", + "version": "0.8.0", + "url": "https://aequilibrae.com/python/V.0.8.0/" + }, + { + "name": "0.7.7", + "version": "0.7.7", + "url": "https://aequilibrae.com/python/V.0.7.7/" + }, + { + "name": "0.7.6", + "version": "0.7.6", + "url": "https://aequilibrae.com/python/V.0.7.6/" + }, + { + "name": "0.7.5", + "version": "0.7.5", + "url": "https://aequilibrae.com/python/V.0.7.5/" + }, + { + "name": "0.7.4", + "version": "0.7.4", + "url": "https://aequilibrae.com/python/V.0.7.4/" + }, + { + "name": "0.7.3", + "version": "0.7.3", + "url": "https://aequilibrae.com/python/V.0.7.3/" + }, + { + "name": "0.7.2", + "version": "0.7.2", + "url": "https://aequilibrae.com/python/V.0.7.2/" + }, + { + "name": "0.7.1", + "version": "0.7.1", + "url": "https://aequilibrae.com/python/V.0.7.1/" + }, + { + "name": "0.7.0", + "version": "0.7.0", + "url": "https://aequilibrae.com/python/V.0.7.0/" + }, + { + "name": "0.6.5", + "version": "0.6.5", + "url": "https://aequilibrae.com/python/V.0.6.5/" + }, + { + "name": "0.6.4", + "version": "0.6.4", + "url": "https://aequilibrae.com/python/V.0.6.4/" + }, + { + "name": "0.6.3", + "version": "0.6.3", + "url": "https://aequilibrae.com/python/V.0.6.3/" + }, + { + "name": "0.6.2", + "version": "0.6.2", + "url": "https://aequilibrae.com/python/V.0.6.2/" + }, + { + "name": "0.6.1", + "version": "0.6.1", + "url": "https://aequilibrae.com/python/V.0.6.1/" + }, + { + "name": "0.6.0", + "version": "0.6.0", + "url": "https://aequilibrae.com/python/V.0.6.0/" + }, + { + "name": "0.5.3", + "version": "0.5.3", + "url": "https://aequilibrae.com/python/V.0.5.3/" + } +] \ No newline at end of file diff --git a/docs/source/api.rst b/docs/source/api.rst index 4bab28da4..64f0f6076 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,33 +1,26 @@ -.. _aequilibrae_api: +.. _api_reference: -API documentation -================= +============= +API Reference +============= .. automodule:: aequilibrae - :no-members: - :no-undoc-members: - :no-inherited-members: - :no-show-inheritance: - -Project Module --------------- - -.. currentmodule:: aequilibrae +Project +------- +.. current_module:: aequilibrae.project .. autosummary:: :nosignatures: - :toctree: _generated + :toctree: generated/ - Project - -Project components -++++++++++++++++++ + Project +Project Components +~~~~~~~~~~~~~~~~~~ .. currentmodule:: aequilibrae.project - .. autosummary:: :nosignatures: - :toctree: _generated + :toctree: generated/ About FieldEditor @@ -36,91 +29,75 @@ Project components Network Zoning -Project objects -++++++++++++++++++ - +Project Objects +~~~~~~~~~~~~~~~ .. currentmodule:: aequilibrae.project - .. autosummary:: :nosignatures: - :toctree: _generated + :toctree: generated/ Zone -Network data -~~~~~~~~~~~~ - +Network Data +------------ .. currentmodule:: aequilibrae.project.network - .. autosummary:: :nosignatures: - :toctree: _generated + :toctree: generated/ Modes LinkTypes Links Nodes -Network items -~~~~~~~~~~~~~ - +Network Items +------------- .. currentmodule:: aequilibrae.project.network - .. autosummary:: :nosignatures: - :toctree: _generated - + :toctree: generated/ Mode LinkType Link Node -Parameters Module ------------------ - +Parameters +---------- .. currentmodule:: aequilibrae - .. autosummary:: :nosignatures: - :toctree: _generated + :toctree: generated/ Parameters -Distribution Module -------------------- - -.. currentmodule:: aequilibrae - +Distribution +------------ +.. currentmodule:: aequilibrae.distribution .. autosummary:: - :nosignatures: - :toctree: _generated + :toctree: generated/ Ipf GravityApplication GravityCalibration SyntheticGravityModel -Matrix Module -------------- - -.. currentmodule:: aequilibrae - +Matrix +------ +.. currentmodule:: aequilibrae.matrix .. autosummary:: :nosignatures: - :toctree: _generated - - AequilibraeMatrix - AequilibraeData - -Paths Module ------------- + :toctree: generated/ -.. currentmodule:: aequilibrae + AequilibraeData + AequilibraeMatrix +Paths +----- +.. currentmodule:: aequilibrae.paths .. autosummary:: :nosignatures: - :toctree: _generated + :toctree: generated/ Graph AssignmentResults @@ -130,22 +107,11 @@ Paths Module TrafficClass TrafficAssignment -Transit Module --------------- - -.. currentmodule:: aequilibrae - +Transit +------- +.. currentmodule:: aequilibrae.transit .. autosummary:: :nosignatures: - :toctree: _generated - - GTFS - create_gtfsdb - -Use examples ------------- - -.. toctree:: - :maxdepth: 1 + :toctree: generated/ - usageexamples \ No newline at end of file + Transit diff --git a/docs/source/conf.py b/docs/source/conf.py index 38e7ad1d0..79ee1208c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,32 +14,35 @@ import os import sys +from datetime import datetime from pathlib import Path +from sphinx_gallery.sorting import ExplicitOrder -import sphinx_theme - -project_dir = Path(__file__).parent.parent +project_dir = Path(__file__).parent.parent.parent if str(project_dir) not in sys.path: sys.path.append(str(project_dir)) # Sometimes this file is exec'd directly from sphinx code... -project_dir = os.path.abspath("../..") +project_dir = os.path.abspath("../../") if str(project_dir) not in sys.path: sys.path.insert(0, project_dir) from __version__ import release_version - # -- Project information ----------------------------------------------------- project = "AequilibraE" -copyright = "2018, Pedro Camargo" +copyright = f"{str(datetime.now().date())}, AequilibraE developers" author = "Pedro Camargo" # The short X.Y version version = release_version -# The full version, including alpha/beta/rc tags -release = "30/07/2018" +if ".dev" in version: + switcher_version = "dev" +elif "rc" in version: + switcher_version = version.split("rc")[0] + " (rc)" +else: + switcher_version = version # -- General configuration --------------------------------------------------- @@ -61,11 +64,23 @@ "sphinx_autodoc_annotation", "sphinx.ext.autosummary", "sphinx_git", + "sphinx_panels", + "sphinx_copybutton" ] +# Change plot_gallery to True to start building examples again sphinx_gallery_conf = { "examples_dirs": ["examples"], # path to your example scripts "gallery_dirs": ["_auto_examples"], # path to where to save gallery generated output + 'capture_repr': ('_repr_html_', '__repr__'), + 'remove_config_comments': True, + "plot_gallery": False, + "subsection_order": ExplicitOrder(["examples/creating_models", + "examples/network_manipulation", + "examples/editing_networks", + "examples/trip_distribution", + "examples/visualization", + "examples/other_applications"]) } # Add any paths that contain templates here, relative to this directory. @@ -85,7 +100,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -94,27 +109,33 @@ # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" -highlight_language = "none" +# highlight_language = "none" + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -# html_theme = "pyramid" -html_theme = "neo_rtd_theme" -html_theme_path = [sphinx_theme.get_html_theme_path(html_theme)] - -# html_theme_options = { -# "body_max_width": '70%', -# 'sidebarwidth': '20%' -# } - +html_theme = "pydata_sphinx_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_theme_options = { + "show_nav_level": 0, + "github_url": "https://github.com/AequilibraE/aequilibrae", + "navbar_end": ["theme-switcher", "version-switcher"], + "switcher": { + "json_url": "/_static/switcher.json", + "version_match": switcher_version, + }, +} + +# The name for this set of Sphinx documents. If None, it defaults to +html_title = f"AequilibraE {version}" + # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. @@ -140,13 +161,10 @@ autodoc_default_options = { "members": "var1, var2", "member-order": "bysource", - "special-members": "__init__", + "special-members": False, "private-members": False, "undoc-members": True, "exclude-members": "__weakref__", - "inherited-members": False, - "show-inheritance": False, - "autodoc_inherit_docstrings": False, } autodoc_member_order = "groupwise" diff --git a/docs/source/developing.rst b/docs/source/developing.rst new file mode 100644 index 000000000..2222d9654 --- /dev/null +++ b/docs/source/developing.rst @@ -0,0 +1,14 @@ +.. _developing_aequilibrae: + +Developing +========== + +This section describes how to contribute to AequilibraE's development and what +is our current roadmap. + +.. toctree:: + :maxdepth: 1 + + development/softwaredevelopment + development/roadmap + \ No newline at end of file diff --git a/docs/source/roadmap.rst b/docs/source/development/roadmap.rst similarity index 60% rename from docs/source/roadmap.rst rename to docs/source/development/roadmap.rst index a0f7f8244..ada5d850f 100644 --- a/docs/source/roadmap.rst +++ b/docs/source/development/roadmap.rst @@ -1,35 +1,32 @@ -Roadmap -======= +.. _development_roadmap: -As AequilibraE is a project with an incredibly small team and no source of +Development Roadmap +=================== + +As AequilibraE is a project with an incredibly small team and very little external funding, it is not feasible to determine a precise schedule for the development -of new features or even a proper roadmap of specific developments. +of new features or even a detailed roadmap. However, there are a number of enhancements to the software that we have already identified and that we intend to dedicate some time to in the future. * Network data model - * Introduce centroid connector data type to replace inference that all links + * Introduce centroid connector data type to replace the inference that all links connected to centroids are connectors * Traffic assignment * Re-development of the path-finding algorithm to allow for turn penalties/bans - * Implementation of network simplification to improve performance of - path-finding * New origin-based traffic assignment to achieve ultra-converged assignment + * New path-finding algorithm based on contraction-hierarchies * Public Transport - * Import of GTFS map-matching it into a project network - * Re-development of Public Transport data model for GTFS/AequilibraE - * Export of GTFS (enables editing of GTFS in QGIS - * Transit path computation (Likely to be the - `CSA `_ or - similar) + * Export of GTFS (enables editing of GTFS in QGIS) + * Public transit assignment (Scheduled for 3rd Quarter 2023) * Project @@ -42,7 +39,7 @@ identified and that we intend to dedicate some time to in the future. * QGIS * Inclusion of TSP and more general vehicle routing problems (resource - constraints, pick-up and delivery, etc.) + constraints, pick-up, and delivery, etc.) If there is any other feature you would like to suggest, please record a new issue on `GitHub `_, or drop diff --git a/docs/source/softwaredevelopment.rst b/docs/source/development/softwaredevelopment.rst similarity index 82% rename from docs/source/softwaredevelopment.rst rename to docs/source/development/softwaredevelopment.rst index 5cb30cab5..1c2e668f1 100644 --- a/docs/source/softwaredevelopment.rst +++ b/docs/source/development/softwaredevelopment.rst @@ -1,8 +1,10 @@ +.. _software_development: + Contributing to AequilibraE =========================== -This page presents some initial instructions on how to setup your system to start contributing to AequilibraE and lists -the requirements for all pull-requests to be merged into master. +This page presents some initial instructions on how to set up your system to start contributing to +AequilibraE and lists the requirements for all pull requests to be merged into master. .. note:: The recommendations on this page are current as of October 2021. @@ -15,11 +17,6 @@ The most important piece of AequilibraE's backend is, without a doubt, `numpy `_. -AequilibraE also observes a strong requirement of only using libraries that are available in the Python installation -used by `QGIS `_ on Windows, as the most important use case of this library is as the computational -backend of the AequilibraE GUI for QGIS. This requirement can be relaxed, but it has to be analysed on a base-by-case -basis and CANNOT break current workflow within QGIS. - We have not yet found an ideal source of recommendations for developing AequilibraE, but a good initial take can be found in `this article. `__ @@ -28,9 +25,10 @@ Development Install As it goes with most Python packages, we recommend using a dedicated virtual environment to develop AequilibraE. -AequilibraE is currently tested for Python 3.7, 3.8 and 3.9, but we recommend using Python 3.8 for development. +AequilibraE is currently tested for Python 3.7, 3.8, 3.9 & 3.11, but we recommend using Python 3.9 or 2.10 for development. -We also assume you are using `PyCharm `_, which is an awesome IDE for Python. +We also assume you are using `PyCharm `_ or `VSCode `_ +which are awesome IDEs for Python. If you are using a different IDE, we would welcome if you could contribute with instructions to set that up. @@ -46,15 +44,15 @@ Windows Make sure to clone the AequilibraE repository and run the following from within that cloned repo using an elevated command prompt. -Python 3.8 (or whatever version you chose) needs to be installed, and the +Python 3.9 (or whatever version you chose) needs to be installed, and the following instructions assume you are using `Chocolatey `_ as a package manager. :: - cinst python3 --version 3.8 + cinst python3 --version 3.9 cinst python - set PATH=C:\Python38;%PATH% + set PATH=C:\Python39;%PATH% python -m pip install pipenv virtualenv .venv #Only if you want to save the virtual environment in the same folder python -m pipenv install --dev @@ -74,7 +72,7 @@ AequilibraE development (tries) to follow a few standards. Since this is largely portions of the code are still not up to such standards. Style -~~~~~~ +~~~~~ * Python code should follow (mostly) the `pycodestyle style guide `_ * Python docstrings should follow the `reStructuredText Docstring Format `_ @@ -87,11 +85,12 @@ Imports * Imports should be one per line. * Imports should be grouped into standard library, third-party, and intra-library imports (`ctrl+shit+o` does it automatically on PyCharm). -* Imports of NumPy should follow the following convention: +* Imports of NumPy and Pandas should follow the following convention: -:: +.. code-block:: python import numpy as np + import pandas as pd Contributing to AequilibraE ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -123,9 +122,9 @@ In a more verbose way... * Unit testing * Build/packaging tests * Documentation building test -* If the tests pass, then a manual pull request can be approved to merge into master. -* The master branch is protected and therefore can only be written to after the code has been reviewed and approved. -* No individual has the privileges to push to the master branch. +* If the tests pass, then a manual pull request can be approved to merge into develop. +* The master and develop branches are protected and therefore can only be written to after the code has been reviewed and approved. +* No individual has the privileges to push to the master or developer branches. Release versions ~~~~~~~~~~~~~~~~~ @@ -155,13 +154,10 @@ Testing AequilibraE testing is done with three tools: * `Flake8 `_, a tool to check Python code style -* `pytest `_, a Python testing tool -* `coveralls `_, a tool for measuring test code coverage +* `Black `_, The uncompromising code formatter -To run the tests locally, you will need to figure out what to do... - - -These same tests are run by Travis with each push to the repository. These tests need to pass in order to somebody +Testing is done for Windows, MacOs and Ubuntu Linux on all supported Python versions, and we use GitHub Actions +to run these tests. These tests need to pass and additionally somebody has to manually review the code before merging it into master (or returning for corrections). In some cases, test targets need to be updated to match the new results produced by the code since these @@ -203,8 +199,8 @@ AequilibraE releases are automatically uploaded to the `Python Package Index `__ (pypi) at each new GitHub release (2 to 6 times per year). -Finally -~~~~~~~~~ +Acknowledgement +~~~~~~~~~~~~~~~ A LOT of the structure around the documentation was borrowed (copied) from the excellent project `ActivitySim `_ \ No newline at end of file diff --git a/docs/source/examples/plot_from_osm.py b/docs/source/examples/creating_models/from_osm.py similarity index 75% rename from docs/source/examples/plot_from_osm.py rename to docs/source/examples/creating_models/from_osm.py index 33e513adc..93d634349 100644 --- a/docs/source/examples/plot_from_osm.py +++ b/docs/source/examples/creating_models/from_osm.py @@ -1,19 +1,22 @@ """ -Project from Open-Street Maps +.. _plot_from_osm: + +Project from OpenStreetMap ============================= -On this example we show how to create an empty project and populate with a network from Open-Street maps +In this example, we show how to create an empty project and populate it with a network from OpenStreetMap. -But this time we will use Folium to visualize the network +This time we will use Folium to visualize the network. """ # %% -## Imports +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join from aequilibrae import Project import folium +# sphinx_gallery_thumbnail_path = 'images/nauru.png' # %% # We create an empty project on an arbitrary folder @@ -24,12 +27,12 @@ # Now we can download the network from any place in the world (as long as you have memory for all the download # and data wrangling that will be done) -# We can create from a bounding box -# or from a named place. For the sake of this example, we will choose the small nation of Nauru +# We can create from a bounding box or a named place. +# For the sake of this example, we will choose the small nation of Nauru. project.network.create_from_osm(place_name="Nauru") # %% -# We grab all the links data as a Pandas dataframe so we can process it easier +# We grab all the links data as a Pandas DataFrame so we can process it easier links = project.network.links.data # We create a Folium layer @@ -61,13 +64,3 @@ # %% project.close() - -# %% -# **Don't know Nauru? Here is a map** - -# %% -from PIL import Image -import matplotlib.pyplot as plt - -img = Image.open("nauru.png") -plt.imshow(img) diff --git a/docs/source/examples/plot_create_zoning.py b/docs/source/examples/creating_models/plot_create_zoning.py similarity index 77% rename from docs/source/examples/plot_create_zoning.py rename to docs/source/examples/creating_models/plot_create_zoning.py index 4b0274652..1a6a49e95 100644 --- a/docs/source/examples/plot_create_zoning.py +++ b/docs/source/examples/creating_models/plot_create_zoning.py @@ -1,31 +1,24 @@ """ +.. _create_zones: + Creating a zone system based on Hex Bins ======================================== -On this example we show how to create a hex bin zones covering an arbitrary area. +In this example, we show how to create hex bin zones covering an arbitrary area. -We use the Nauru example to create roughly 100 zones covering the whole modelling +We use the Nauru example to create roughly 100 zones covering the whole modeling area as delimited by the entire network You are obviously welcome to create whatever zone system you would like, as long as -you have the geometries for them. In that case, you can just skip the Hex bin computation +you have the geometries for them. In that case, you can just skip the hex bin computation part of this notebook. We also add centroid connectors to our network to make it a pretty complete example -""" - -# %% -# **What we want to create a zoning system like this** -# %% -from PIL import Image -import matplotlib.pyplot as plt - -img = Image.open("plot_create_zoning.png") -plt.imshow(img) +""" # %% -## Imports +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join @@ -33,6 +26,7 @@ from shapely.geometry import Point import shapely.wkb from aequilibrae.utils.create_example import create_example +# sphinx_gallery_thumbnail_path = "images/plot_create_zoning.png" # %% # We create an empty project on an arbitrary folder @@ -45,32 +39,30 @@ # We said we wanted 100 zones zones = 100 -# %% md -# Hex Bins using SpatiaLite - - -# %% md -#### Spatialite requires a few things to compute hex bins +#%% +# Hex Bins using Spatialite +# ------------------------- # %% -# One of the them is the area you want to cover +# Spatialite requires a few things to compute hex bins. +# One of them is the area you want to cover. network = project.network -# So we use the convenient network method convex_hull() (it may take some time for very large networks) +# So we use the convenient network method ``convex_hull()`` (it may take some time for very large networks) geo = network.convex_hull() # %% -# The second thing is the side of the hexbin, which we can compute from its area -# The approximate area of the desired hexbin is +# The second thing is the side of the hex bin, which we can compute from its area. +# The approximate area of the desired hex bin is zone_area = geo.area / zones # Since the area of the hexagon is **3 * sqrt(3) * side^2 / 2** # is side is equal to **sqrt(2 * sqrt(3) * A/9)** zone_side = sqrt(2 * sqrt(3) * zone_area / 9) # %% -# Now we can run an sql query to compute the hexagonal grid -# There are many ways to create Hex bins (including with a GUI on QGIS), but we find that -# using SpatiaLite is a pretty neat solution +# Now we can run an SQL query to compute the hexagonal grid. +# There are many ways to create hex bins (including with a GUI on QGIS), but we find that +# using SpatiaLite is a pretty neat solution. # For which we will use the entire network bounding box to make sure we cover everything extent = network.extent() @@ -108,8 +100,9 @@ zone.add_centroid(None) -# %% md -## Centroid connectors +#%% +# Centroid connectors +# ------------------- # %% for zone_id, zone in zoning.all_zones().items(): @@ -125,8 +118,8 @@ break # %% -# Let's add an special generator zones -# We also add a centroid at the airport terminal +# Let's add special generator zones +# We also add a centroid at the airport terminal nodes = project.network.nodes # Let's use some silly number for its ID, like 10,000, just so we can easily differentiate it @@ -134,15 +127,10 @@ airport.geometry = Point(166.91749582, -0.54472590) airport.save() - # When connecting a centroid not associated with a zone, we need to tell AequilibraE what is the initial area around -# the centroid that needs to be considered when looking for candidate nodes +# the centroid that needs to be considered when looking for candidate nodes. # Distance here is in degrees, so 0.01 is equivalent to roughly 1.1km airport.connect_mode(airport.geometry.buffer(0.01), mode_id="c", link_types="ytrusP", connectors=1) - -# %% - - # %% project.close() diff --git a/docs/source/examples/plot_from_layer.py b/docs/source/examples/creating_models/plot_from_layer.py similarity index 78% rename from docs/source/examples/plot_from_layer.py rename to docs/source/examples/creating_models/plot_from_layer.py index 68f8f07b8..fa5f2c21d 100644 --- a/docs/source/examples/plot_from_layer.py +++ b/docs/source/examples/creating_models/plot_from_layer.py @@ -1,32 +1,36 @@ """ +.. _project_from_link_layer: + Project from a link layer ========================= -On this example we show how to create an empty project and populate it with a +In this example, we show how to create an empty project and populate it with a network coming from a link layer we load from a text file. It can easily be -replaced with a different form of loading the data (GeoPandas, for example) +replaced with a different form of loading the data (GeoPandas, for example). -We use Folium to visualize the resulting network +We use Folium to visualize the resulting network. """ # %% -## Imports +# Imports from uuid import uuid4 +import urllib.request +from string import ascii_lowercase from tempfile import gettempdir from os.path import join -from aequilibrae import Project from shapely.wkt import loads as load_wkt import pandas as pd import folium -import requests -import urllib.request -from string import ascii_lowercase + +from aequilibrae import Project +# sphinx_gallery_thumbnail_path = 'images/plot_from_layer.png' # %% # We create an empty project on an arbitrary folder fldr = join(gettempdir(), uuid4().hex) project = Project() project.new(fldr) + # %% # Now we obtain the link data for our example (in this case from a link layer # we will download from the AequilibraE website) @@ -48,8 +52,7 @@ existing_types = [ltype.link_type for ltype in lt_dict.values()] # We could also get it directly from the project database -# existing_types = [x[0] for x in project.conn.execute('Select link_type from link_types')] - +# ``existing_types = [x[0] for x in project.conn.execute('Select link_type from link_types')]`` # %% # We add the link types that do not exist yet @@ -75,7 +78,7 @@ # Now let's see the modes we have in the network that we DON'T have already in # the model -# We get all the unique mode combinations and merge into a single string +# We get all the unique mode combinations and merge them into a single string all_variations_string = "".join(df.modes.unique()) # We then get all the unique modes in that string above @@ -93,7 +96,7 @@ new_mode.mode_name = f"Mode_from_original_data_{mode_id}" # new_type.description = 'Your custom description here if you have one' - # It is a little different, because you need to add it to the project + # It is a little different because you need to add it to the project project.network.modes.add(new_mode) new_mode.save() @@ -110,7 +113,8 @@ links.refresh_fields() # %% -## We can now add all links to the project! +# We can now add all links to the project! + for idx, record in df.iterrows(): new_link = links.new() @@ -123,10 +127,8 @@ new_link.geometry = load_wkt(record.WKT) new_link.save() -# -# # %% -# We grab all the links data as a Pandas dataframe so we can process it easier +# We grab all the links data as a Pandas DataFrame so we can process it easier links = project.network.links.data # We create a Folium layer @@ -137,7 +139,7 @@ for i, row in links.iterrows(): points = row.geometry.wkt.replace("LINESTRING ", "").replace("(", "").replace(")", "").split(", ") points = "[[" + "],[".join([p.replace(" ", ", ") for p in points]) + "]]" - # we need to take from x/y to lat/long + # We need to take from x/y to lat/long points = [[x[1], x[0]] for x in eval(points)] line = folium.vector_layers.PolyLine( @@ -151,28 +153,10 @@ long, lat = curr.fetchone() # %% -map_osm = folium.Map(location=[lat, long], zoom_start=14) +map_osm = folium.Map(location=[lat, long], zoom_start=15) network_links.add_to(map_osm) folium.LayerControl().add_to(map_osm) map_osm -# + # %% project.close() -# -# %% -# **Don't know Queluz? Here is a picture of its most impressive urban structure** - -from PIL import Image -import matplotlib.pyplot as plt - -pic = "https://upload.wikimedia.org/wikipedia/commons/2/2c/Ponte_Governador_Mario_Covas_01.jpg" -pic_local = join(fldr, "queluz.jpg") -headers = {"User-Agent": "AequilibraE (https://aequilibrae.com/; contact@aequilibrae.com) python-library/0.7"} - -response = requests.get(pic, headers=headers) -if response.status_code == 200: - with open(pic_local, "wb") as f: - f.write(response.content) - - img = Image.open(pic_local) - plt.imshow(img) diff --git a/docs/source/examples/plot_import_from_gmns.py b/docs/source/examples/creating_models/plot_import_from_gmns.py similarity index 91% rename from docs/source/examples/plot_import_from_gmns.py rename to docs/source/examples/creating_models/plot_import_from_gmns.py index 82a4e51a8..c31706b9c 100644 --- a/docs/source/examples/plot_import_from_gmns.py +++ b/docs/source/examples/creating_models/plot_import_from_gmns.py @@ -1,20 +1,23 @@ """ +.. _import_from_gmns: + Importing network from GMNS =========================== In this example, we import a simple network in GMNS format. -The source files of this network is publicly available in the GMNS GitHub repository itself. +The source files of this network are publicly available in the GMNS GitHub repository itself. Here's the repository: https://github.com/zephyr-data-specs/GMNS """ # %% -## Imports +# Imports from uuid import uuid4 from os.path import join from tempfile import gettempdir from aequilibrae.project import Project from aequilibrae.parameters import Parameters import folium +# sphinx_gallery_thumbnail_path = 'images/plot_import_from_gmns.png' # %% # We load the example file from the GMNS GitHub repository @@ -39,7 +42,7 @@ } new_node_fields = { "port": {"description": "port flag", "type": "text", "required": False}, - "hospital": {"description": "hoospital flag", "type": "text", "required": False}, + "hospital": {"description": "hospital flag", "type": "text", "required": False}, } par = Parameters() @@ -48,7 +51,7 @@ par.write_back() # %% -# As it is specified in that the geometries are in the coordinate system EPSG:32619, +# As it is specified that the geometries are in the coordinate system EPSG:32619, # which is different than the system supported by AequilibraE (EPSG:4326), we inform # the srid in the method call: project.network.create_from_gmns( @@ -102,11 +105,10 @@ long, lat = curr.fetchone() # %% - # We create the map map_gmns = folium.Map(location=[lat, long], zoom_start=17) -# add all layers +# Add all layers for layer in layers: layer.add_to(map_gmns) @@ -114,6 +116,5 @@ folium.LayerControl().add_to(map_gmns) map_gmns - # %% project.close() diff --git a/docs/source/examples/plot_import_gtfs.py b/docs/source/examples/creating_models/plot_import_gtfs.py similarity index 57% rename from docs/source/examples/plot_import_gtfs.py rename to docs/source/examples/creating_models/plot_import_gtfs.py index 881c98a78..ff103dfb3 100644 --- a/docs/source/examples/plot_import_gtfs.py +++ b/docs/source/examples/creating_models/plot_import_gtfs.py @@ -1,14 +1,15 @@ """ +.. _example_gtfs: + Import GTFS =========== -On this example, we import a GTFS feed to our model. We will also perform map matching. +In this example, we import a GTFS feed to our model and perform map matching. We use data from Coquimbo, a city in La Serena Metropolitan Area in Chile. - """ # %% -## Imports +# Imports from uuid import uuid4 from os import remove from os.path import join @@ -20,61 +21,58 @@ from aequilibrae.transit import Transit from aequilibrae.utils.create_example import create_example +# sphinx_gallery_thumbnail_path = 'images/plot_import_gtfs.png' -"""Let's create an empty project on an arbitrary folder.""" # %% +# Let's create an empty project on an arbitrary folder. fldr = join(gettempdir(), uuid4().hex) project = create_example(fldr, "coquimbo") -""" -As Coquimbo example already has a complete GTFS model, we shall remove its public transport -database for the sake of this example. -""" # %% +# As the Coquimbo example already has a complete GTFS model, we shall remove its public transport +# database for the sake of this example. + remove(join(fldr, "public_transport.sqlite")) -"""Let's import the GTFS feed.""" # %% - +# Let's import the GTFS feed. dest_path = join(fldr, "gtfs_coquimbo.zip") -""" -Now we create our Transit object and import the GTFS feed into our model. -This will automatically create a new public transport database. -""" + # %% +# Now we create our Transit object and import the GTFS feed into our model. +# This will automatically create a new public transport database. + data = Transit(project) transit = data.new_gtfs_builder(agency="LISANCO", file_path=dest_path) -""" -To load the data, we must chose one date. We're going to continue with 2016-04-13, but feel free -to experiment any other available dates. Transit class has a function which allows you to check -the available dates for the GTFS feed. -It should take approximately 2 minutes to load the data. -""" # %% +# To load the data, we must choose one date. We're going to continue with 2016-04-13 but feel free +# to experiment with any other available dates. Transit class has a function allowing you to check +# dates for the GTFS feed. It should take approximately 2 minutes to load the data. + transit.load_date("2016-04-13") -""" -Now we execute the map matching to find the real paths. -Depending on the GTFS size, this process can be really time consuming. -""" # %% +# Now we execute the map matching to find the real paths. +# Depending on the GTFS size, this process can be really time-consuming. + transit.set_allow_map_match(True) transit.map_match() -"""Finally, we save our GTFS into our model.""" # %% +# Finally, we save our GTFS into our model. transit.save_to_disk() -""" -Now we will plot the route we just imported into our model! -""" +# %% +# Now we will plot one of the route's patterns we just imported conn = database_connection("transit") -links = pd.read_sql("SELECT seq, ST_AsText(geometry) geom FROM pattern_mapping WHERE geom IS NOT NULL;", con=conn) +links = pd.read_sql( + "SELECT pattern_id, ST_AsText(geometry) geom FROM routes WHERE geom IS NOT NULL AND pattern_id == 10001003000;", + con=conn) stops = pd.read_sql("""SELECT stop_id, ST_X(geometry) X, ST_Y(geometry) Y FROM stops""", con=conn) @@ -85,11 +83,11 @@ layers = [gtfs_links, gtfs_stops] for i, row in links.iterrows(): - points = row.geom.replace("LINESTRING", "").replace("(", "").replace(")", "").split(", ") + points = row.geom.replace("MULTILINESTRING", "").replace("(", "").replace(")", "").split(", ") points = "[[" + "],[".join([p.replace(" ", ", ") for p in points]) + "]]" points = [[x[1], x[0]] for x in eval(points)] - _ = folium.vector_layers.PolyLine(points, popup=f"link_id: {row.seq}", color="red", weight=2).add_to( + _ = folium.vector_layers.PolyLine(points, popup=f"link_id: {row.pattern_id}", color="red", weight=2).add_to( gtfs_links ) @@ -100,7 +98,7 @@ point, popup=f"link_id: {row.stop_id}", color="black", - radius=5, + radius=3, fill=True, fillColor="black", fillOpacity=1.0, diff --git a/docs/source/examples/creating_models/readme.rst b/docs/source/examples/creating_models/readme.rst new file mode 100644 index 000000000..eb329b356 --- /dev/null +++ b/docs/source/examples/creating_models/readme.rst @@ -0,0 +1,4 @@ +.. _examples_creating_models: + +Creating Models +--------------- diff --git a/docs/source/examples/plot_moving_link_extremity.py b/docs/source/examples/editing_networks/plot_moving_link_extremity.py similarity index 86% rename from docs/source/examples/plot_moving_link_extremity.py rename to docs/source/examples/editing_networks/plot_moving_link_extremity.py index 44248e15e..ad5bfb779 100644 --- a/docs/source/examples/plot_moving_link_extremity.py +++ b/docs/source/examples/editing_networks/plot_moving_link_extremity.py @@ -1,12 +1,15 @@ """ +.. _editing_network_links: + Editing network geometry: Links =============================== -On this example we move a link extremity from one point to another -and see what happens to the network +In this example, we move a link extremity from one point to another +and see what happens to the network. """ -## Imports +# %% +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join @@ -14,6 +17,7 @@ from shapely.geometry import LineString, Point import matplotlib.pyplot as plt +# %% # We create the example project inside our temp folder fldr = join(gettempdir(), uuid4().hex) @@ -57,12 +61,11 @@ for nid in curr.fetchall(): geo = all_nodes.get(nid[0]).geometry - plt.plot(*geo.xy, "ro", color="black") + plt.plot(*geo.xy, "o", color="black") plt.show() -# Now look at the network how it used to be - +# Now look at the network and how it used to be # %% project.close() diff --git a/docs/source/examples/plot_moving_nodes.py b/docs/source/examples/editing_networks/plot_moving_nodes.py similarity index 88% rename from docs/source/examples/plot_moving_nodes.py rename to docs/source/examples/editing_networks/plot_moving_nodes.py index abfa4ce29..8c8af619f 100644 --- a/docs/source/examples/plot_moving_nodes.py +++ b/docs/source/examples/editing_networks/plot_moving_nodes.py @@ -1,12 +1,15 @@ """ +.. _editing_network_nodes: + Editing network geometry: Nodes =============================== -On this example we show how to mode a node in the network and look into +In this example, we show how to mode a node in the network and look into what happens to the links. """ -## Imports +# %% +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join @@ -14,6 +17,7 @@ from shapely.geometry import Point import matplotlib.pyplot as plt +# %% # We create the example project inside our temp folder fldr = join(gettempdir(), uuid4().hex) @@ -44,7 +48,7 @@ geo = links.get(lid[0]).geometry plt.plot(*geo.xy, color="blue") -plt.plot(*node.geometry.xy, "ro", color="black") +plt.plot(*node.geometry.xy, "o", color="black") plt.show() @@ -52,5 +56,4 @@ # Look at the original network and see how it used to look like # %% - project.close() diff --git a/docs/source/examples/plot_splitting_link.py b/docs/source/examples/editing_networks/plot_splitting_link.py similarity index 86% rename from docs/source/examples/plot_splitting_link.py rename to docs/source/examples/editing_networks/plot_splitting_link.py index 92fde1555..0e118e9b5 100644 --- a/docs/source/examples/plot_splitting_link.py +++ b/docs/source/examples/editing_networks/plot_splitting_link.py @@ -1,12 +1,14 @@ """ +.. _editing_network_splitting_link: + Editing network geometry: Splitting link ======================================== -In this example we split a link right in the middle, while keeping all fields -in the database equal. Distance is proportionally computed automatically in the database +In this example, we split a link right in the middle, while keeping all fields +in the database equal. Distance is proportionally computed automatically in the database. """ - -## Imports +# %% +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join @@ -56,7 +58,7 @@ for node in [link.a_node, link.b_node, new_link.b_node]: geo = all_nodes.get(node).geometry - plt.plot(*geo.xy, "ro", color="black") + plt.plot(*geo.xy, "o", color="black") plt.show() # %% @@ -75,10 +77,9 @@ for nid in curr.fetchall(): geo = all_nodes.get(nid[0]).geometry - plt.plot(*geo.xy, "ro", color="black") + plt.plot(*geo.xy, "o", color="black") plt.show() # %% - project.close() diff --git a/docs/source/examples/editing_networks/readme.rst b/docs/source/examples/editing_networks/readme.rst new file mode 100644 index 000000000..c9c8a1463 --- /dev/null +++ b/docs/source/examples/editing_networks/readme.rst @@ -0,0 +1,5 @@ +.. _examples_editing_networks: + +Editing networks +---------------- + diff --git a/docs/source/examples/network_manipulation/readme.rst b/docs/source/examples/network_manipulation/readme.rst new file mode 100644 index 000000000..cec834864 --- /dev/null +++ b/docs/source/examples/network_manipulation/readme.rst @@ -0,0 +1,4 @@ +.. _examples_net_manipulation: + +Network Manipulation +-------------------- diff --git a/docs/source/examples/plot_export_to_gmns.py b/docs/source/examples/other_applications/plot_export_to_gmns.py similarity index 93% rename from docs/source/examples/plot_export_to_gmns.py rename to docs/source/examples/other_applications/plot_export_to_gmns.py index cee8a039e..e9eda5d2f 100644 --- a/docs/source/examples/plot_export_to_gmns.py +++ b/docs/source/examples/other_applications/plot_export_to_gmns.py @@ -1,21 +1,24 @@ """ +.. _export_to_gmns: + Exporting network to GMNS =========================== In this example, we export a simple network to GMNS format. The source AequilibraE model used as input for this is the result of the import process -(create_from_gmns()) using the GMNS example of Arlington Signals, which can be found +(``create_from_gmns()``) using the GMNS example of Arlington Signals, which can be found in the GMNS repository on GitHub: https://github.com/zephyr-data-specs/GMNS """ # %% -## Imports +# Imports from uuid import uuid4 import os from tempfile import gettempdir from aequilibrae.utils.create_example import create_example import pandas as pd import folium +# sphinx_gallery_thumbnail_path = 'images/plot_export_to_gmns.png' # %% # We load the example project inside a temp folder diff --git a/docs/source/examples/plot_find_disconnected.py b/docs/source/examples/other_applications/plot_find_disconnected.py similarity index 87% rename from docs/source/examples/plot_find_disconnected.py rename to docs/source/examples/other_applications/plot_find_disconnected.py index 9d7f6a531..239476116 100644 --- a/docs/source/examples/plot_find_disconnected.py +++ b/docs/source/examples/other_applications/plot_find_disconnected.py @@ -1,29 +1,24 @@ """ +.. _find_disconnected_links: + Finding disconnected links ========================== -On this example, we show how to find disconnected links in an AequilibraE network +In this example, we show how to find disconnected links in an AequilibraE network We use the Nauru example to find disconnected links """ - # %% -import numpy as np -import pandas as pd -from PIL import Image -import matplotlib.pyplot as plt - -img = Image.open("disconnected_network.png") -plt.imshow(img) - -# %% -## Imports +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join from datetime import datetime +import pandas as pd +import numpy as np from aequilibrae.utils.create_example import create_example from aequilibrae.paths.results import PathResults +# sphinx_gallery_thumbnail_path = 'images/disconnected_network.png' # %% # We create an empty project on an arbitrary folder @@ -32,11 +27,11 @@ # Let's use the Nauru example project for display project = create_example(fldr, "nauru") -# Let's analyze the mode car, or 'c' in our model +# Let's analyze the mode car or 'c' in our model mode = "c" # %% -# We need to create the graph, but before that we need to have at least one centroid in our network +# We need to create the graph, but before that, we need to have at least one centroid in our network # We get an arbitrary node to set as centroid and allow for the construction of graphs centroid_count = project.conn.execute("select count(*) from nodes where is_centroid=1").fetchone()[0] @@ -54,7 +49,7 @@ graph.set_blocked_centroid_flows(False) if centroid_count == 0: - # Let's revert of setting up that node as centroid in case we had to do it + # Let's revert to setting up that node as centroid in case we had to do it nd.is_centroid = 0 nd.save() @@ -102,6 +97,7 @@ # And save to disk alongside our model islands.to_csv(join(fldr, "island_outputs_complete.csv"), index=False) +#%% # If you join the node_id field in the csv file generated above with the a_node or b_node fields # in the links table, you will have the corresponding links in each disjoint island found diff --git a/docs/source/examples/plot_logging_to_terminal.py b/docs/source/examples/other_applications/plot_logging_to_terminal.py similarity index 72% rename from docs/source/examples/plot_logging_to_terminal.py rename to docs/source/examples/other_applications/plot_logging_to_terminal.py index 4d9e90f2c..db87f98e9 100644 --- a/docs/source/examples/plot_logging_to_terminal.py +++ b/docs/source/examples/other_applications/plot_logging_to_terminal.py @@ -1,18 +1,21 @@ """ +.. _logging_to_terminal: + Logging to terminal =================== -On this example we show how to make all log messages show in the terminal. +In this example, we show how to make all log messages show in the terminal. """ # %% -## Imports +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join from aequilibrae.utils.create_example import create_example import logging import sys +# sphinx_gallery_thumbnail_path = 'images/plot_logging_to_terminal_image.png' # %% # We create the example project inside our temp folder @@ -29,13 +32,3 @@ # %% project.close() - -# %% -# **Want to see what you will get?** - -# %% -from PIL import Image -import matplotlib.pyplot as plt - -img = Image.open("plot_logging_to_terminal_image.png") -plt.imshow(img) diff --git a/docs/source/examples/other_applications/readme.rst b/docs/source/examples/other_applications/readme.rst new file mode 100644 index 000000000..1b3e3aab8 --- /dev/null +++ b/docs/source/examples/other_applications/readme.rst @@ -0,0 +1,4 @@ +.. _examples_other_applications: + +Other Applications +------------------ diff --git a/docs/source/examples/readme.rst b/docs/source/examples/readme.rst index 498c6134c..6e42b1ef1 100644 --- a/docs/source/examples/readme.rst +++ b/docs/source/examples/readme.rst @@ -1,5 +1,5 @@ -Workflows -========= +Examples +======== -A series of different workflows using AequilibraE's main features +A series of different examples using AequilibraE's main features diff --git a/docs/source/examples/trip_distribution/just_matrices.omx b/docs/source/examples/trip_distribution/just_matrices.omx new file mode 100644 index 000000000..67345d255 Binary files /dev/null and b/docs/source/examples/trip_distribution/just_matrices.omx differ diff --git a/docs/source/examples/trip_distribution/plot_assignment_without_model.py b/docs/source/examples/trip_distribution/plot_assignment_without_model.py new file mode 100644 index 000000000..4ef285c1b --- /dev/null +++ b/docs/source/examples/trip_distribution/plot_assignment_without_model.py @@ -0,0 +1,124 @@ +""" +.. _plot_assignment_without_model: + +Traffic Assignment without an AequilibraE Model +=============================================== + +In this example, we show how to perform Traffic Assignment in AequilibraE without a model. + +We are using `Sioux Falls data `_, from TNTP. + +""" +# %% +# Imports +import os +import pandas as pd +import numpy as np +from tempfile import gettempdir + +from aequilibrae.matrix import AequilibraeMatrix +from aequilibrae.paths import Graph +from aequilibrae.paths import TrafficAssignment +from aequilibrae.paths.traffic_class import TrafficClass + +# %% +# We load the example file from the GMNS GitHub repository +net_file = "https://raw.githubusercontent.com/bstabler/TransportationNetworks/master/SiouxFalls/SiouxFalls_net.tntp" + +demand_file = "https://raw.githubusercontent.com/bstabler/TransportationNetworks/master/SiouxFalls/CSV-data/SiouxFalls_od.csv" + +# %% +# Let's use a temporary folder to store our data +folder = gettempdir() + +# %% +# First we load our demand file. This file has three columns: O, D, and Ton. +# O and D stand for origin and destination, respectively, and Ton is the demand of each +# OD pair. + +dem = pd.read_csv(demand_file) +zones = int(max(dem.O.max(), dem.D.max())) +index = np.arange(zones) + 1 + +# %% +# Since our OD-matrix is in a different shape than we expect (for Sioux Falls, that +# would be a 24x24 matrix), we must create our matrix. +mtx = np.zeros(shape=(zones, zones)) +for element in dem.to_records(index=False): + mtx[element[0]-1][element[1]-1] = element[2] + +# %% +# Now let's create an AequilibraE Matrix with out data +aemfile = os.path.join(folder, "demand.aem") +aem = AequilibraeMatrix() +kwargs = {'file_name': aemfile, + 'zones': zones, + 'matrix_names': ['matrix'], + "memory_only": False} # We'll save it to disk so we can use it later + +aem.create_empty(**kwargs) +aem.matrix['matrix'][:,:] = mtx[:,:] +aem.index[:] = index[:] + +# %% +# Let's import information about our network. As we're loading data in TNTP format, +# we should do these manipulations. +net = pd.read_csv(net_file, skiprows=2, sep="\t", lineterminator=";", header=None) + +net.columns = ["newline", "a_node", "b_node", "capacity", "length", "free_flow_time", "b", "power", "speed", "toll", "link_type", "terminator"] + +net.drop(columns=["newline", "terminator"], index=[76], inplace=True) + +# %% +network = net[['a_node', 'b_node', "capacity", 'free_flow_time', "b", "power"]] +network = network.assign(direction=1) +network["link_id"] = network.index + 1 +network = network.astype({"a_node":"int64", "b_node": "int64"}) + +#%% +# Let's build our Graph! In case you're in doubt about the Graph, `click here ` +# to read more about it. +# %% +g = Graph() +g.cost = network['free_flow_time'].values +g.capacity = network['capacity'].values +g.free_flow_time = network['free_flow_time'].values + +g.network = network +g.network_ok = True +g.status = 'OK' +g.prepare_graph(index) +g.set_graph("free_flow_time") +g.cost = np.array(g.cost, copy=True) +g.set_skimming(["free_flow_time"]) +g.set_blocked_centroid_flows(False) +g.network["id"] = g.network.link_id + +# %% +# Let's perform our assignment. Feel free to try different algorithms, +# as well as change the maximum number of iterations and the gap. +aem = AequilibraeMatrix() +aem.load(aemfile) +aem.computational_view(["matrix"]) + +assigclass = TrafficClass("car", g, aem) + +assig = TrafficAssignment() + +assig.set_classes([assigclass]) +assig.set_vdf("BPR") +assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) +assig.set_capacity_field("capacity") +assig.set_time_field("free_flow_time") +assig.set_algorithm("fw") +assig.max_iter = 100 +assig.rgap_target = 1e-6 +assig.execute() + +# %% +# Now let's take a look at the Assignment results +print(assig.results()) + +# %% +# And at the Assignment report +print(assig.report()) \ No newline at end of file diff --git a/docs/source/examples/plot_forecasting.py b/docs/source/examples/trip_distribution/plot_forecasting.py similarity index 77% rename from docs/source/examples/plot_forecasting.py rename to docs/source/examples/trip_distribution/plot_forecasting.py index a3ad9732f..d97269b55 100644 --- a/docs/source/examples/plot_forecasting.py +++ b/docs/source/examples/trip_distribution/plot_forecasting.py @@ -1,12 +1,14 @@ """ +.. _example_usage_forecasting: + Forecasting ============ -On this example we present a full forecasting workflow for the Sioux Falls +In this example, we present a full forecasting workflow for the Sioux Falls example model. """ - -## Imports +# %% +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join @@ -21,67 +23,63 @@ project = create_example(fldr) logger = project.logger -# We get the project open, we can tell the logger to direct all messages to the terminal as well +# We get the project open, and we can tell the logger to direct all messages to the terminal as well stdout_handler = logging.StreamHandler(sys.stdout) formatter = logging.Formatter("%(asctime)s;%(levelname)s ; %(message)s") stdout_handler.setFormatter(formatter) logger.addHandler(stdout_handler) -# %% md - -## Traffic assignment with skimming +#%% +# Traffic assignment with skimming +# -------------------------------- # %% - from aequilibrae.paths import TrafficAssignment, TrafficClass # %% - -# we build all graphs +# We build all graphs project.network.build_graphs() -# We get warnings that several fields in the project are filled with NaNs. Which is true, but we won't use those fields +# We get warnings that several fields in the project are filled with NaNs. +# This is true, but we won't use those fields # %% - -# we grab the graph for cars +# We grab the graph for cars graph = project.network.graphs["c"] -# let's say we want to minimize free_flow_time +# Let's say we want to minimize the free_flow_time graph.set_graph("free_flow_time") # And will skim time and distance while we are at it graph.set_skimming(["free_flow_time", "distance"]) -# And we will allow paths to be compute going through other centroids/centroid connectors +# And we will allow paths to be computed going through other centroids/centroid connectors # required for the Sioux Falls network, as all nodes are centroids graph.set_blocked_centroid_flows(False) # %% - # We get the demand matrix directly from the project record -# so let's inspect what we have in the project +# So let's inspect what we have in the project proj_matrices = project.matrices -proj_matrices.list() +print(proj_matrices.list()) # %% - # Let's get it in this better way demand = proj_matrices.get_matrix("demand_omx") demand.computational_view(["matrix"]) # %% - assig = TrafficAssignment() -# Creates the assignment class +# Create the assignment class assigclass = TrafficClass(name="car", graph=graph, matrix=demand) # The first thing to do is to add at list of traffic classes to be assigned assig.add_class(assigclass) # We set these parameters only after adding one class to the assignment -assig.set_vdf("BPR") # This is not case-sensitive # Then we set the volume delay function +assig.set_vdf("BPR") # This is not case-sensitive +# Then we set the volume delay function assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) # And its parameters assig.set_capacity_field("capacity") # The capacity and free flow travel times as they exist in the graph @@ -90,56 +88,47 @@ # And the algorithm we want to use to assign assig.set_algorithm("bfw") -# since I haven't checked the parameters file, let's make sure convergence criteria is good +# Since I haven't checked the parameters file, let's make sure convergence criteria is good assig.max_iter = 1000 assig.rgap_target = 0.001 assig.execute() # we then execute the assignment # %% - # Convergence report is easy to see import pandas as pd convergence_report = assig.report() -convergence_report.head() +print(convergence_report.head()) # %% - volumes = assig.results() -volumes.head() +print(volumes.head()) # %% - # We could export it to CSV or AequilibraE data, but let's put it directly into the results database assig.save_results("base_year_assignment") # %% - # And save the skims assig.save_skims("base_year_assignment_skims", which_ones="all", format="omx") -# %% md - -## Trip distribution - -# %% md - -### Calibration +#%% +# Trip distribution +# ----------------- +# Calibration +# ~~~~~~~~~~~ # We will calibrate synthetic gravity models using the skims for TIME that we just generated # %% - import numpy as np from aequilibrae.distribution import GravityCalibration # %% - # Let's take another look at what we have in terms of matrices in the model -proj_matrices.list() +print(proj_matrices.list()) # %% - # We need the demand demand = proj_matrices.get_matrix("demand_aem") @@ -147,14 +136,12 @@ imped = proj_matrices.get_matrix("base_year_assignment_skims_car") # %% - # We can check which matrix cores were created for our skims to decide which one to use imped.names -# Where free_flow_time_final is actually the congested time for the last iteration +# Where ``free_flow_time_final`` is actually the congested time for the last iteration # %% - # But before using the data, let's get some impedance for the intrazonals # Let's assume it is 75% of the closest zone imped_core = "free_flow_time_final" @@ -172,51 +159,45 @@ np.fill_diagonal(imped.matrix_view, intrazonals) # %% - # Since we are working with an OMX file, we cannot overwrite a matrix on disk # So we give a new name to save it imped.save(names=["final_time_with_intrazonals"]) # %% - # This also updates these new matrices as those being used for computation # As one can verify below imped.view_names # %% - # We set the matrices for being used in computation demand.computational_view(["matrix"]) # %% - for function in ["power", "expo"]: gc = GravityCalibration(matrix=demand, impedance=imped, function=function, nan_as_zero=True) gc.calibrate() model = gc.model - # we save the model + # We save the model model.save(join(fldr, f"{function}_model.mod")) # We can save the result of applying the model as well - # we can also save the calibration report + # We can also save the calibration report with open(join(fldr, f"{function}_convergence.log"), "w") as otp: for r in gc.report: otp.write(r + "\n") -# %% md - -## Forecast -# * We create a set of * 'future' * vectors using some random growth factors -# * We apply the model for inverse power, as the TFLD seems to be a better fit for the actual one +#%% +# Forecast +# -------- +# We create a set of 'future' vectors using some random growth factors. +# We apply the model for inverse power, as the trip frequency length distribution +# (TFLD) seems to be a better fit for the actual one. # %% - from aequilibrae.distribution import Ipf, GravityApplication, SyntheticGravityModel from aequilibrae.matrix import AequilibraeData -import numpy as np # %% - # We compute the vectors from our matrix origins = np.sum(demand.matrix_view, axis=1) destinations = np.sum(demand.matrix_view, axis=0) @@ -234,28 +215,28 @@ vectors.index[:] = demand.index[:] -# Then grow them with some random growth between 0 and 10% - Plus balance them +# Then grow them with some random growth between 0 and 10%, and balance them vectors.origins[:] = origins * (1 + np.random.rand(vectors.entries) / 10) vectors.destinations[:] = destinations * (1 + np.random.rand(vectors.entries) / 10) vectors.destinations *= vectors.origins.sum() / vectors.destinations.sum() -# %% - +#%% # Impedance +# ~~~~~~~~~ + +# %% imped = proj_matrices.get_matrix("base_year_assignment_skims_car") imped.computational_view(["final_time_with_intrazonals"]) # If we wanted the main diagonal to not be considered... -# np.fill_diagonal(imped.matrix_view, np.nan) +# ``np.fill_diagonal(imped.matrix_view, np.nan)`` # %% - for function in ["power", "expo"]: model = SyntheticGravityModel() model.load(join(fldr, f"{function}_model.mod")) outmatrix = join(proj_matrices.fldr, f"demand_{function}_model.aem") - apply = GravityApplication() args = { "impedance": imped, "rows": vectors, @@ -273,14 +254,13 @@ gravity.save_to_project(name=f"demand_{function}_modeled", file_name=f"demand_{function}_modeled.omx") # %% - # We update the matrices table/records and verify that the new matrices are indeed there proj_matrices.update_database() -proj_matrices.list() - -# %% md +print(proj_matrices.list()) -### We now run IPF for the future vectors +#%% +# IPF for the future vectors +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ # %% args = { @@ -299,30 +279,25 @@ ipf.save_to_project(name="demand_ipfd_omx", file_name="demand_ipfd.omx") # %% +df = proj_matrices.list() -proj_matrices.list() - -# %% md - -## Future traffic assignment +#%% +# Future traffic assignment +# ------------------------- # %% - from aequilibrae.paths import TrafficAssignment, TrafficClass # %% - logger.info("\n\n\n TRAFFIC ASSIGNMENT FOR FUTURE YEAR") # %% - demand = proj_matrices.get_matrix("demand_ipfd") -# let's see what is the core we ended up getting. It should be 'gravity' +# Let's see what is the core we ended up getting. It should be 'gravity' demand.names # %% - # Let's use the IPF matrix demand.computational_view("matrix") @@ -331,11 +306,12 @@ # Creates the assignment class assigclass = TrafficClass(name="car", graph=graph, matrix=demand) -# The first thing to do is to add at list of traffic classes to be assigned +# The first thing to do is to add at a list of traffic classes to be assigned assig.add_class(assigclass) -assig.set_vdf("BPR") # This is not case-sensitive # Then we set the volume delay function +assig.set_vdf("BPR") # This is not case-sensitive +# Then we set the volume delay function assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) # And its parameters assig.set_capacity_field("capacity") # The capacity and free flow travel times as they exist in the graph @@ -344,30 +320,33 @@ # And the algorithm we want to use to assign assig.set_algorithm("bfw") -# since I haven't checked the parameters file, let's make sure convergence criteria is good +# Since I haven't checked the parameters file, let's make sure convergence criteria is good assig.max_iter = 500 assig.rgap_target = 0.00001 -# %% +#%% +# **OPTIONAL** -# OPTIONAL: If we want to execute select link analysis on a particular TrafficClass, we set the links we are analysing +# If we want to execute select link analysis on a particular TrafficClass, we set the links we are analyzing. # The format of the input select links is a dictionary (str: list[tuple]). -# Each entry represents a separate set of selected links to compute. The str name will name the set of links -# The list[tuple] is the list of links being selected, of the form (link_id, direction), as it occurs in the Graph -# direction can be 0, 1, -1. 0 denotes bi-directionality -# For example, let's use Select Link on two sets of links, +# Each entry represents a separate set of selected links to compute. The str name will name the set of links. +# The list[tuple] is the list of links being selected, of the form (link_id, direction), as it occurs in the Graph. +# Direction can be 0, 1, -1. 0 denotes bi-directionality +# For example, let's use Select Link on two sets of links: + +# %% select_links = { "Leaving node 1": [(1, 1), (2, 1)], "Random nodes": [(3, 1), (5, 1)], } -# We call this command on the class we are analysing with our dictionary of values +# We call this command on the class we are analyzing with our dictionary of values assigclass.set_select_links(select_links) assig.execute() # we then execute the assignment # %% # Now let us save our select link results, all we need to do is provide it with a name -# In additional to exporting the select link flows, it also exports the Select Link matrices in OMX format. +# In addition to exporting the select link flows, it also exports the Select Link matrices in OMX format. assig.save_select_link_results("select_link_analysis") # %% @@ -384,8 +363,9 @@ # And save the skims assig.save_skims("future_year_assignment_skims", which_ones="all", format="omx") -# %% md +#%% # We can also plot convergence +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ import matplotlib.pyplot as plt df = assig.report() diff --git a/docs/source/examples/trip_distribution/plot_ipf_without_model.py b/docs/source/examples/trip_distribution/plot_ipf_without_model.py new file mode 100644 index 000000000..26777779c --- /dev/null +++ b/docs/source/examples/trip_distribution/plot_ipf_without_model.py @@ -0,0 +1,74 @@ +""" +.. _plot_ipf_without_model: + +Running IPF without an AequilibraE model +======================================== + +In this example, we show you how to use AequilibraE's IPF function without a model. +This is a compliment to the application in :ref:`Trip Distribution `. + +Let's consider that you have an OD-matrix, the future production and future attraction values. +*How would your trip distribution matrix using IPF look like?* +The data used in this example comes from Table 5.6 in [ORW2011]_. +""" + +# %% +# Imports +import numpy as np + +from aequilibrae.distribution import Ipf +from os.path import join +from tempfile import gettempdir +from aequilibrae.matrix import AequilibraeMatrix, AequilibraeData + +# %% +folder = gettempdir() + +# %% +matrix = np.array([[5, 50, 100, 200], [50, 5, 100, 300], [50, 100, 5, 100], [100, 200, 250, 20]], dtype="float64") +future_prod = np.array([400, 460, 400, 702], dtype="float64") +future_attr = np.array([260, 400, 500, 802], dtype="float64") + +num_zones = matrix.shape[0] + +# %% +mtx = AequilibraeMatrix() +mtx.create_empty(file_name=join(folder, "matrix.aem"), zones=num_zones) +mtx.index[:] = np.arange(1, num_zones + 1)[:] +mtx.matrices[:, :, 0] = matrix[:] +mtx.computational_view() + +# %% +args = { + "entries": mtx.index.shape[0], + "field_names": ["productions", "attractions"], + "data_types": [np.float64, np.float64], + "file_path": join(folder, "vectors.aem"), +} + +vectors = AequilibraeData() +vectors.create_empty(**args) + +vectors.productions[:] = future_prod[:] +vectors.attractions[:] = future_attr[:] + +vectors.index[:] = mtx.index[:] + +# %% +args = { + "matrix": mtx, + "rows": vectors, + "row_field": "productions", + "columns": vectors, + "column_field": "attractions", + "nan_as_zero": True, +} +fratar = Ipf(**args) +fratar.fit() + +# %% +fratar.output.matrix_view + +# %% +for line in fratar.report: + print(line) diff --git a/docs/source/examples/plot_path_and_skimming.py b/docs/source/examples/trip_distribution/plot_path_and_skimming.py similarity index 82% rename from docs/source/examples/plot_path_and_skimming.py rename to docs/source/examples/trip_distribution/plot_path_and_skimming.py index e4789d325..1c3e9d847 100644 --- a/docs/source/examples/plot_path_and_skimming.py +++ b/docs/source/examples/trip_distribution/plot_path_and_skimming.py @@ -1,12 +1,14 @@ """ +.. _example_usage_paths: + Path and skimming ================= -On this example we show how to perform path computation and network skimming +In this example, we show how to perform path computation and network skimming for the Sioux Falls example model. """ -## Imports +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join @@ -21,42 +23,46 @@ import logging import sys -# We the project open, we can tell the logger to direct all messages to the terminal as well +# We the project opens, we can tell the logger to direct all messages to the terminal as well logger = project.logger stdout_handler = logging.StreamHandler(sys.stdout) formatter = logging.Formatter("%(asctime)s;%(levelname)s ; %(message)s") stdout_handler.setFormatter(formatter) logger.addHandler(stdout_handler) +#%% +# Path Computation +# ---------------- + # %% -# imports from aequilibrae.paths import PathResults # %% -# we build all graphs +# We build all graphs project.network.build_graphs() -# We get warnings that several fields in the project are filled with NaNs. Which is true, but we won't use those fields +# We get warnings that several fields in the project are filled with NaNs. +# This is true, but we won't use those fields. # %% -# we grab the graph for cars +# We grab the graph for cars graph = project.network.graphs["c"] # we also see what graphs are available # project.network.graphs.keys() -# let's say we want to minimize distance +# let's say we want to minimize the distance graph.set_graph("distance") # And will skim time and distance while we are at it graph.set_skimming(["free_flow_time", "distance"]) -# And we will allow paths to be compute going through other centroids/centroid connectors +# And we will allow paths to be computed going through other centroids/centroid connectors # required for the Sioux Falls network, as all nodes are centroids # BE CAREFUL WITH THIS SETTING graph.set_blocked_centroid_flows(False) # %% -# instantiate a path results object and prepare it to work with the graph +# Let's instantiate a path results object and prepare it to work with the graph res = PathResults() res.prepare(graph) @@ -80,7 +86,7 @@ # %% -# If we want to compute the path for a different destination and same origin, we can just do this +# If we want to compute the path for a different destination and the same origin, we can just do this # It is way faster when you have large networks res.update_trace(13) @@ -94,6 +100,7 @@ import matplotlib.pyplot as plt from shapely.ops import linemerge +# %% links = project.network.links # We plot the entire network @@ -108,12 +115,11 @@ plt.plot(*path_geometry.xy, color="blue", linestyle="dashed", linewidth=2) plt.show() -# %% md - -## Now to skimming +#%% +# Now to skimming +# --------------- # %% - from aequilibrae.paths import NetworkSkimming # %% @@ -128,7 +134,6 @@ skm.execute() # %% - # The result is an AequilibraEMatrix object skims = skm.results.skims diff --git a/docs/source/examples/plot_trip_distribution.py b/docs/source/examples/trip_distribution/plot_trip_distribution.py similarity index 89% rename from docs/source/examples/plot_trip_distribution.py rename to docs/source/examples/trip_distribution/plot_trip_distribution.py index 9d776009c..1938aee2b 100644 --- a/docs/source/examples/plot_trip_distribution.py +++ b/docs/source/examples/trip_distribution/plot_trip_distribution.py @@ -1,11 +1,13 @@ """ +.. _example_usage_distribution: + Trip Distribution ================= -On this example we calibrate a Synthetic Gravity Model that same model plus IPF (Fratar/Furness). +In this example, we calibrate a Synthetic Gravity Model that same model plus IPF (Fratar/Furness). """ -## Imports +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join @@ -24,7 +26,7 @@ # We get the demand matrix directly from the project record # so let's inspect what we have in the project proj_matrices = project.matrices -proj_matrices.list() +print(proj_matrices.list()) # %% @@ -61,7 +63,7 @@ def plot_tlfd(demand, skim, name): return plt -# %%\ +# %% from aequilibrae.distribution import GravityCalibration # %% @@ -70,14 +72,14 @@ def plot_tlfd(demand, skim, name): gc = GravityCalibration(matrix=demand, impedance=impedance, function=function, nan_as_zero=True) gc.calibrate() model = gc.model - # we save the model + # We save the model model.save(join(fldr, f"{function}_model.mod")) # We can save an image for the resulting model _ = plot_tlfd(gc.result_matrix.matrix_view, impedance.matrix_view, join(fldr, f"{function}_tfld.png")) # We can save the result of applying the model as well - # we can also save the calibration report + # We can also save the calibration report with open(join(fldr, f"{function}_convergence.log"), "w") as otp: for r in gc.report: otp.write(r + "\n") @@ -87,11 +89,11 @@ def plot_tlfd(demand, skim, name): plt = plot_tlfd(demand.matrix_view, impedance.matrix_view, join(fldr, "demand_tfld.png")) plt.show() -# %% md - -## Forecast -# * We create a set of * 'future' * vectors by applying some models -# * We apply the model for both deterrence functions +#%% +# Forecast +# -------- +# We create a set of *'future'* vectors by applying some models +# and apply the model for both deterrence functions # %% @@ -104,7 +106,6 @@ def plot_tlfd(demand, skim, name): zonal_data = pd.read_sql("Select zone_id, population, employment from zones order by zone_id", project.conn) # We compute the vectors from our matrix - args = { "file_path": join(fldr, "synthetic_future_vector.aed"), "entries": demand.zones, @@ -131,7 +132,6 @@ def plot_tlfd(demand, skim, name): model.load(join(fldr, f"{function}_model.mod")) outmatrix = join(proj_matrices.fldr, f"demand_{function}_model.aem") - apply = GravityApplication() args = { "impedance": impedance, "rows": vectors, @@ -152,11 +152,12 @@ def plot_tlfd(demand, skim, name): # We update the matrices table/records and verify that the new matrices are indeed there proj_matrices.update_database() -proj_matrices.list() +print(proj_matrices.list()) # %% md -### We now run IPF for the future vectors +# %% +# We now run IPF for the future vectors # %% args = { @@ -176,7 +177,7 @@ def plot_tlfd(demand, skim, name): # %% -proj_matrices.list() +print(proj_matrices.list()) # %% diff --git a/docs/source/examples/trip_distribution/readme.rst b/docs/source/examples/trip_distribution/readme.rst new file mode 100644 index 000000000..67ddcc5bb --- /dev/null +++ b/docs/source/examples/trip_distribution/readme.rst @@ -0,0 +1,4 @@ +.. _examples_trip_distribution: + +Trip Distribution +----------------- diff --git a/docs/source/examples/trip_distribution/select_link_analysis.omx b/docs/source/examples/trip_distribution/select_link_analysis.omx new file mode 100644 index 000000000..2c97a0e39 Binary files /dev/null and b/docs/source/examples/trip_distribution/select_link_analysis.omx differ diff --git a/docs/source/examples/plot_delaunay_lines.py b/docs/source/examples/visualization/plot_delaunay_lines.py similarity index 86% rename from docs/source/examples/plot_delaunay_lines.py rename to docs/source/examples/visualization/plot_delaunay_lines.py index bd6622295..18e5bbdc1 100644 --- a/docs/source/examples/plot_delaunay_lines.py +++ b/docs/source/examples/visualization/plot_delaunay_lines.py @@ -1,12 +1,14 @@ """ +.. _creating_delaunay_lines: + Creating Delaunay Lines ======================= -On this example we show how to create AequilibraE's famous Delaunay Lines, but in Python +In this example, we show how to create AequilibraE's famous Delaunay Lines, but in Python. -For more on this topic, the firs publication is here: https://xl-optim.com/delaunay/ +For more on this topic, the first publication is `here `_. -We use the Sioux-Falls example once again. +We use the Sioux Falls example once again. """ # %% @@ -49,7 +51,6 @@ # %% # Now we get the matrix we want and create the Delaunay Lines - links = pd.read_sql("Select link_id, st_asBinary(geometry) geometry from delaunay_network", project.conn) links.geometry = links.geometry.apply(shapely.wkb.loads) links.set_index("link_id", inplace=True) diff --git a/docs/source/examples/plot_display.py b/docs/source/examples/visualization/plot_display.py similarity index 92% rename from docs/source/examples/plot_display.py rename to docs/source/examples/visualization/plot_display.py index afcde06e9..32a34b14a 100644 --- a/docs/source/examples/plot_display.py +++ b/docs/source/examples/visualization/plot_display.py @@ -1,31 +1,22 @@ """ +.. _explore_network_on_notebook: + Exploring the network on a notebook =================================== -On this example we show how to use Folium to plot a network for different modes +In this example, we show how to use Folium to plot a network for different modes. We will need Folium for this example, and we will focus on creating a layer for -each mode in the network, a layer for all links and a layer for all nodes +each mode in the network, a layer for all links and a layer for all nodes. """ - -# %% -# **What we want is a map that looks a little like this** - -# %% -from PIL import Image -import matplotlib.pyplot as plt - -img = Image.open("plot_network_image.png") -plt.imshow(img) - - # %% -## Imports +# Imports from uuid import uuid4 from tempfile import gettempdir from os.path import join from aequilibrae.utils.create_example import create_example import folium +# sphinx_gallery_thumbnail_path = 'images/plot_network_image.png' # %% # We create an empty project on an arbitrary folder diff --git a/docs/source/examples/visualization/readme.rst b/docs/source/examples/visualization/readme.rst new file mode 100644 index 000000000..f19667ded --- /dev/null +++ b/docs/source/examples/visualization/readme.rst @@ -0,0 +1,5 @@ +.. _examples-visualization: + +Visualization +------------- +Examples in this session allows the user to plot some data visualization. \ No newline at end of file diff --git a/docs/source/gettingstarted.rst b/docs/source/getting_started.rst similarity index 51% rename from docs/source/gettingstarted.rst rename to docs/source/getting_started.rst index a7d026aac..0cfaf6b1d 100644 --- a/docs/source/gettingstarted.rst +++ b/docs/source/getting_started.rst @@ -1,9 +1,10 @@ .. _getting_started: -Getting Started +Getting started =============== -This page describes how to get started with AequilibraE. +In this section we describe how to install AequilibraE. +If you have already installed AequilibraE, you can move on to the :ref:`first steps ` in modeling with AequilibraE page. .. note:: Although AequilibraE is under intense development, we try to avoid making @@ -11,15 +12,15 @@ This page describes how to get started with AequilibraE. and possible API changes often. .. note:: - The recommendations on this page are current as of August 2021. + The recommendations on this page are current as of April 2023. .. index:: installation Installation ------------ -1. Install `Python 3.7, 3.8 or 3.9 `__. We recommend Python - 3.8. +1. Install `Python 3.7, 3.8, 3.9, 3.10 or 3.11 `__. We recommend Python + 3.9 or 3.10. 2. Install AequilibraE @@ -32,60 +33,48 @@ Installation Dependencies ~~~~~~~~~~~~ -Aequilibrae relies on a series of compiled libraries, such as NumPy and Scipy. -If you are working on Windows and have trouble installing any of the -requirements, you can look at -`Christoph Gohlke's wonderful repository `_ -of compiled Python packages for windows. Particularly on Python 3.9, it may be -necessary to resort to Christoph's binaries. - -OMX support -+++++++++++ -AequilibraE also supports OMX starting on version 0.5.3, but that comes with a -few extra dependencies. Installing **openmatrix** solves all those dependencies: - -:: - - pip install openmatrix +All of AequilibraE's dependencies are readily available from `PyPI +`_ for all currently supported Python versions and major +platforms. .. _installing_spatialite_on_windows: Spatialite ++++++++++ -Although the presence of Spatialite is rather obiquitous in the GIS ecosystem, -it has to be installed separately from Python or AequilibraE. +Although the presence of Spatialite is rather ubiquitous in the GIS ecosystem, +it has to be installed separately from Python or AequilibraE in any platform. This `blog post `_ has a more comprehensive explanation of what is the setup you need to get Spatialite working, -but below there is something you can start with. +but that is superfluous if all you want is to get it working. Windows ^^^^^^^ + +.. note:: + On Windows ONLY, AequilibraE automatically verifies if you have SpatiaLite + installed in your system and downloads it to your temporary folder if you do + not. + Spatialite does not have great support on Python for Windows. For this reason, -it is necessary to download Spatialite for Windows and inform AequilibraE of its -location. +it is necessary to download Spatialite for Windows and inform and load it +to the Python SQLite driver every time you connect to the database. One can download the appropriate version of the latest SpatiaLite release directly from its `project page `_ , or the cached versions on AequilibraE's website for -`64-Bit Python `_ -or -`32-Bit Python `_ +`64-Bit Python `_ -After unpacking the zip file into its own folder (say D:/spatialite), one can -start their Python session by creating a *temporary* environment variable with said -location, as follows: +After unpacking the zip file into its own folder (say *D:/spatialite*), one can +**temporarily** add the spatialite folder to system path environment variable, +as follows: :: import os - from aequilibrae.utils.create_example import create_example - os.environ['PATH'] = 'D:/spatialite' + ';' + os.environ['PATH'] - project = create_example(fldr, 'nauru') - For a permanent recording of the Spatialite location on your system, please refer to the blog post referenced above or Windows-specific documentation. @@ -96,8 +85,9 @@ On Ubuntu it is possible to install Spatialite by simply using apt-get :: - sudo apt-get install libsqlite3-mod-spatialite - sudo apt-get install -y libspatialite-dev + sudo apt update -y + sudo apt install -y libsqlite3-mod-spatialite + sudo apt install -y libspatialite-dev MacOS @@ -125,7 +115,14 @@ things to keep an eye on are: Substantial testing has been done with large real-world models (up to 8,000 zones) and memory requirements did not exceed the traditional 32Gb found in most -modelling computers these days. In most cases 16Gb of RAM is enough even for -large models (5,000+ zones). Parallelization is fully implemented for path -computation, and can make use of as many CPUs as there are available in the -system when doing traffic assignment. +modeling computers these days. In most cases 16Gb of RAM is enough even for +large models (5,000+ zones). Computationally intensive procedures such as +skimming and traffic assignment have been parallelized, so AequilibraE can make +use of as many CPUs as there are available in the system for such procedures. + +.. toctree:: + :maxdepth: 1 + :hidden: + + getting_started/first_steps + getting_started/first_model diff --git a/docs/source/getting_started/first_steps.rst b/docs/source/getting_started/first_steps.rst new file mode 100644 index 000000000..29dd58bb1 --- /dev/null +++ b/docs/source/getting_started/first_steps.rst @@ -0,0 +1,28 @@ +.. _first_steps: + +First steps +=========== + +AequilibraE is a python package for transportation modeling. To take full +advantage of its capacities, it would be great if you already have a little +knowledge on transportation modeling. If you're totaly new to the area, +we encourage you to do some research on your own or take a look in some useful +books and research papers in the :ref:`useful links ` session. + +If you're already feeling confortable with some transportation modeling concepts, +you can move on to :ref:`creating your first AequilibraE model `. + +.. _useful_links: +Useful links +------------ + +In case you're still feeling lost about the implementations of AequilibraE, +we encourage you to take a look into the following references. + +Books & Research Papers +~~~~~~~~~~~~~~~~~~~~~~~ +.. [ORW2011] ORTÚZAR, J.D., WILLUMSEN, L.G. (2011) *Modelling Transport* (4th ed.). Wiley-Blackwell. + +Videos +~~~~~~ +* `An AequilibraE Overview `_ diff --git a/docs/source/images/about_table_example.png b/docs/source/images/about_table_example.png new file mode 100644 index 000000000..db2128d3d Binary files /dev/null and b/docs/source/images/about_table_example.png differ diff --git a/docs/source/images/anaheim_bfw-500_iter.png b/docs/source/images/anaheim_bfw-500_iter.png index 825029088..e1dcb33ab 100644 Binary files a/docs/source/images/anaheim_bfw-500_iter.png and b/docs/source/images/anaheim_bfw-500_iter.png differ diff --git a/docs/source/images/anaheim_cfw-500_iter.png b/docs/source/images/anaheim_cfw-500_iter.png index 76807a109..843911c66 100644 Binary files a/docs/source/images/anaheim_cfw-500_iter.png and b/docs/source/images/anaheim_cfw-500_iter.png differ diff --git a/docs/source/images/anaheim_frank-wolfe-500_iter.png b/docs/source/images/anaheim_frank-wolfe-500_iter.png index 4b9fcc7d1..3d580771c 100644 Binary files a/docs/source/images/anaheim_frank-wolfe-500_iter.png and b/docs/source/images/anaheim_frank-wolfe-500_iter.png differ diff --git a/docs/source/images/anaheim_msa-500_iter.png b/docs/source/images/anaheim_msa-500_iter.png index 6ef6a67f6..2ff88380e 100644 Binary files a/docs/source/images/anaheim_msa-500_iter.png and b/docs/source/images/anaheim_msa-500_iter.png differ diff --git a/docs/source/images/barcelona_bfw-500_iter.png b/docs/source/images/barcelona_bfw-500_iter.png index 378951165..33e7e58d9 100644 Binary files a/docs/source/images/barcelona_bfw-500_iter.png and b/docs/source/images/barcelona_bfw-500_iter.png differ diff --git a/docs/source/images/barcelona_cfw-500_iter.png b/docs/source/images/barcelona_cfw-500_iter.png index 3e816609d..d4d558b5d 100644 Binary files a/docs/source/images/barcelona_cfw-500_iter.png and b/docs/source/images/barcelona_cfw-500_iter.png differ diff --git a/docs/source/images/barcelona_frank-wolfe-500_iter.png b/docs/source/images/barcelona_frank-wolfe-500_iter.png index ee45557ff..db8aadcae 100644 Binary files a/docs/source/images/barcelona_frank-wolfe-500_iter.png and b/docs/source/images/barcelona_frank-wolfe-500_iter.png differ diff --git a/docs/source/images/barcelona_msa-500_iter.png b/docs/source/images/barcelona_msa-500_iter.png index 16e18783d..4e898ebc4 100644 Binary files a/docs/source/images/barcelona_msa-500_iter.png and b/docs/source/images/barcelona_msa-500_iter.png differ diff --git a/docs/source/images/chicago_regional_bfw-500_iter.png b/docs/source/images/chicago_regional_bfw-500_iter.png index 7d574c76e..04a461349 100644 Binary files a/docs/source/images/chicago_regional_bfw-500_iter.png and b/docs/source/images/chicago_regional_bfw-500_iter.png differ diff --git a/docs/source/images/chicago_regional_cfw-500_iter.png b/docs/source/images/chicago_regional_cfw-500_iter.png index 267a1cebc..01058181a 100644 Binary files a/docs/source/images/chicago_regional_cfw-500_iter.png and b/docs/source/images/chicago_regional_cfw-500_iter.png differ diff --git a/docs/source/images/chicago_regional_frank-wolfe-500_iter.png b/docs/source/images/chicago_regional_frank-wolfe-500_iter.png index ee3987fe4..652ad64e3 100644 Binary files a/docs/source/images/chicago_regional_frank-wolfe-500_iter.png and b/docs/source/images/chicago_regional_frank-wolfe-500_iter.png differ diff --git a/docs/source/images/chicago_regional_msa-500_iter.png b/docs/source/images/chicago_regional_msa-500_iter.png index b5a160782..cd6a80c36 100644 Binary files a/docs/source/images/chicago_regional_msa-500_iter.png and b/docs/source/images/chicago_regional_msa-500_iter.png differ diff --git a/docs/source/examples/disconnected_network.png b/docs/source/images/disconnected_network.png similarity index 100% rename from docs/source/examples/disconnected_network.png rename to docs/source/images/disconnected_network.png diff --git a/docs/source/images/ipf_runtime_aequilibrae_vs_benchmark.png b/docs/source/images/ipf_runtime_aequilibrae_vs_benchmark.png new file mode 100644 index 000000000..1250ae9e9 Binary files /dev/null and b/docs/source/images/ipf_runtime_aequilibrae_vs_benchmark.png differ diff --git a/docs/source/images/ipf_runtime_vs_num_cores.png b/docs/source/images/ipf_runtime_vs_num_cores.png new file mode 100644 index 000000000..2d8bf9a19 Binary files /dev/null and b/docs/source/images/ipf_runtime_vs_num_cores.png differ diff --git a/docs/source/images/link_types_table.png b/docs/source/images/link_types_table.png new file mode 100644 index 000000000..bf6cd082e Binary files /dev/null and b/docs/source/images/link_types_table.png differ diff --git a/docs/source/images/matrices_table.png b/docs/source/images/matrices_table.png new file mode 100644 index 000000000..db7b2bc27 Binary files /dev/null and b/docs/source/images/matrices_table.png differ diff --git a/docs/source/images/modes_table.png b/docs/source/images/modes_table.png index 992ea3ae5..b92297528 100644 Binary files a/docs/source/images/modes_table.png and b/docs/source/images/modes_table.png differ diff --git a/docs/source/examples/nauru.png b/docs/source/images/nauru.png similarity index 100% rename from docs/source/examples/nauru.png rename to docs/source/images/nauru.png diff --git a/docs/source/images/parameter_yaml_files_gmns.png b/docs/source/images/parameter_yaml_files_gmns.png new file mode 100644 index 000000000..533826de0 Binary files /dev/null and b/docs/source/images/parameter_yaml_files_gmns.png differ diff --git a/docs/source/examples/plot_create_zoning.png b/docs/source/images/plot_create_zoning.png similarity index 100% rename from docs/source/examples/plot_create_zoning.png rename to docs/source/images/plot_create_zoning.png diff --git a/docs/source/images/plot_export_to_gmns.png b/docs/source/images/plot_export_to_gmns.png new file mode 100644 index 000000000..7748c84d8 Binary files /dev/null and b/docs/source/images/plot_export_to_gmns.png differ diff --git a/docs/source/images/plot_from_layer.png b/docs/source/images/plot_from_layer.png new file mode 100644 index 000000000..83d2d8a52 Binary files /dev/null and b/docs/source/images/plot_from_layer.png differ diff --git a/docs/source/images/plot_import_from_gmns.png b/docs/source/images/plot_import_from_gmns.png new file mode 100644 index 000000000..955580661 Binary files /dev/null and b/docs/source/images/plot_import_from_gmns.png differ diff --git a/docs/source/images/plot_import_gtfs.png b/docs/source/images/plot_import_gtfs.png new file mode 100644 index 000000000..38d18811e Binary files /dev/null and b/docs/source/images/plot_import_gtfs.png differ diff --git a/docs/source/examples/plot_logging_to_terminal_image.png b/docs/source/images/plot_logging_to_terminal_image.png similarity index 100% rename from docs/source/examples/plot_logging_to_terminal_image.png rename to docs/source/images/plot_logging_to_terminal_image.png diff --git a/docs/source/examples/plot_network_image.png b/docs/source/images/plot_network_image.png similarity index 100% rename from docs/source/examples/plot_network_image.png rename to docs/source/images/plot_network_image.png diff --git a/docs/source/images/project_structure.png b/docs/source/images/project_structure.png new file mode 100644 index 000000000..68978e396 Binary files /dev/null and b/docs/source/images/project_structure.png differ diff --git a/docs/source/images/results_table.png b/docs/source/images/results_table.png new file mode 100644 index 000000000..fc3428aec Binary files /dev/null and b/docs/source/images/results_table.png differ diff --git a/docs/source/images/sioux_falls_bfw-500_iter.png b/docs/source/images/sioux_falls_bfw-500_iter.png index 515dedab0..6bb66b41f 100644 Binary files a/docs/source/images/sioux_falls_bfw-500_iter.png and b/docs/source/images/sioux_falls_bfw-500_iter.png differ diff --git a/docs/source/images/sioux_falls_cfw-500_iter.png b/docs/source/images/sioux_falls_cfw-500_iter.png index 5058d907f..a5e947bae 100644 Binary files a/docs/source/images/sioux_falls_cfw-500_iter.png and b/docs/source/images/sioux_falls_cfw-500_iter.png differ diff --git a/docs/source/images/sioux_falls_frank-wolfe-500_iter.png b/docs/source/images/sioux_falls_frank-wolfe-500_iter.png index e1a509420..b76f3c53d 100644 Binary files a/docs/source/images/sioux_falls_frank-wolfe-500_iter.png and b/docs/source/images/sioux_falls_frank-wolfe-500_iter.png differ diff --git a/docs/source/images/sioux_falls_msa-500_iter.png b/docs/source/images/sioux_falls_msa-500_iter.png index 68e6bae13..df0e7a83e 100644 Binary files a/docs/source/images/sioux_falls_msa-500_iter.png and b/docs/source/images/sioux_falls_msa-500_iter.png differ diff --git a/docs/source/images/winnipeg_bfw-500_iter.png b/docs/source/images/winnipeg_bfw-500_iter.png index 1377413f8..6d429b449 100644 Binary files a/docs/source/images/winnipeg_bfw-500_iter.png and b/docs/source/images/winnipeg_bfw-500_iter.png differ diff --git a/docs/source/images/winnipeg_cfw-500_iter.png b/docs/source/images/winnipeg_cfw-500_iter.png index 3993c2d86..6f1d33cbc 100644 Binary files a/docs/source/images/winnipeg_cfw-500_iter.png and b/docs/source/images/winnipeg_cfw-500_iter.png differ diff --git a/docs/source/images/winnipeg_frank-wolfe-500_iter.png b/docs/source/images/winnipeg_frank-wolfe-500_iter.png index a0ce1ad3c..39b93fc5f 100644 Binary files a/docs/source/images/winnipeg_frank-wolfe-500_iter.png and b/docs/source/images/winnipeg_frank-wolfe-500_iter.png differ diff --git a/docs/source/images/winnipeg_msa-500_iter.png b/docs/source/images/winnipeg_msa-500_iter.png index d68d4843a..f4d2dbd0c 100644 Binary files a/docs/source/images/winnipeg_msa-500_iter.png and b/docs/source/images/winnipeg_msa-500_iter.png differ diff --git a/docs/source/index.rst b/docs/source/index.rst index cbf3ad7ee..825339d5c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,6 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +=========== AequilibraE =========== @@ -10,37 +11,121 @@ AequilibraE is the first comprehensive Python package for transportation modeling, and it aims to provide all the resources not easily available from other open-source packages in the Python (NumPy, really) ecosystem. -If you are looking for the documentation for AequilibraE for QGIS, you can -see it on its own webpage `aequilibrae for QGIS 3 `__ +.. seealso:: + + If you are looking for the documentation for **AequilibraE for QGIS**, you can + see it on its `own webpage `_. + +.. panels:: + :card: text-center bg-transparent border-light + :container: container pb-3 + :header: text-center border-light + :footer: border-light + :column: col-lg-6 col-md-6 col-sm-6 col-xs-12 p-2 + + --- + :column: col-lg-12 p-2 + :fa:`folder` + **Getting Started** + ^^^^^^^^^^^^^^^^^ + + New to AequilibraE? Get started here! + + +++++++++++++++++ + + .. link-button:: getting_started + :type: ref + :text: Getting Started + :classes: btn-block btn-secondary stretched-link + + --- + :column: col-lg-12 p-2 + :fa:`folder` + **Using AequilibraE** + ^^^^^^^^^^^^^^^^^ + + A series of examples on how to use AequilibraE, from building a model from scratch + to editing an existing, performing trip distribution or traffic assignment to analyzing + results. + + +++++++++++++++++ + + .. link-button:: _auto_examples/index + :type: ref + :text: Examples + :classes: btn-block btn-secondary stretched-link + + --- + :fa:`book` + **Modeling with AequilibraE** + ^^^^^^^^^^^^^^^^^^^^^^^^^ + + An in-depth guide to modeling with AequilibraE, including the concepts that guide its + development and user-experience. + + +++++++++++++++++ -Examples -======== + .. link-button:: modeling_with_aequilibrae + :type: ref + :text: Modeling with AequilibraE + :classes: btn-block btn-secondary stretched-link + + --- -Skip the *blah-blah-blah* and go straight to the point: :ref:`sphx_glr__auto_examples` + :fa:`tools` + **API References** + ^^^^^^^^^^^^^^ -or to :ref:`getting_started` if you are new to Python or are having trouble with Spatialite + Reference guide to AequilibraE's API. + +++++++++++++++++ + + .. link-button:: api + :type: ref + :text: API references + :classes: btn-block btn-secondary stretched-link + + --- + + :fa:`check` + **Software Validation & Benchmarking** + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Want to see how we test the software for correctness and performance? + + ++++++++++++++++++++++++++++++++++ + + .. link-button:: validation + :type: ref + :text: Validation & Benchmarking + :classes: btn-block btn-secondary stretched-link + + --- + + :fa:`lightbulb` + **Developing** + ^^^^^^^^^^^^ + + Looking for something more than using the software? Check out the development info. + + ++++++++++++ + + .. link-button:: developing + :type: ref + :text: Developing + :classes: btn-block btn-secondary stretched-link -Contents -======== -.. sectnum:: .. toctree:: - :numbered: + :hidden: :maxdepth: 1 - :caption: Contents: - overview - gettingstarted - project - modeling - path_computation_engine - api + getting_started _auto_examples/index - softwaredevelopment - roadmap - qgis - + modeling_with_aequilibrae + api + validation + developing Note to users ============= @@ -50,52 +135,18 @@ any funding or profit from this work, so if your organization is making use of AequilibraE, please consider funding some of the new developments or maintenance of the project. -**We appreciate if you do not send questions directly to the developers**, but +**We appreciate it if you do not send questions directly to the developers**, but there are two alternatives for support: -1. Posting your question to `StackOverflow `_ +1. Posting your question to `StackOverflow `_; 2. Joining the `AequilibraE Google Group `_ and sending your question there. -Aequilibrae **does not have paid support** but if you are looking to hire its developers +AequilibraE **does not have paid support** but if you are looking to hire its developers for specific projects or to **fund AequilibraE's** development, please e-mail the developers at contact@aequilibrae.com. +.. note:: -Version history -=============== - -AequilibraE has been evolving quite fast, so we recommend you upgrading to a -newer version as soon as you can. In the meantime, you can find the -documentation for all versions since 0.5.3. - -* `0.5.3 `_ -* `0.6.0 `_ -* `0.6.1 `_ -* `0.6.2 `_ -* `0.6.3 `_ -* `0.6.4 `_ -* `0.6.5 `_ -* `0.7.0 `_ -* `0.7.1 `_ -* `0.7.2 `_ -* `0.7.3 `_ -* `0.7.4 `_ -* `0.7.5 `_ -* `0.7.6 `_ -* `0.7.7 `_ -* `0.8.0 `_ -* `0.8.1 `_ -* `0.8.2 `_ -* `0.8.3 `_ -* `0.8.4 `_ - -* `Develop Branch (upcoming version) `_ -This documentation correspond to software version: - -.. git_commit_detail:: - :branch: - :commit: - :sha_length: 10 - :uncommitted: - :untracked: + If you want to check any of AequilibraE's versions before 0.9.0, you can checkout their + documentation in :ref:`this page ` \ No newline at end of file diff --git a/docs/source/modeling.rst b/docs/source/modeling.rst deleted file mode 100644 index c6c5a6796..000000000 --- a/docs/source/modeling.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. _modeling_platform: - -Modeling Platform -================= - -AequilibraE's main objective is to be a fully-fledged modeling platform, and -therefore most of its features are geared towards that. In time, this section -of the documentation will have more detailed documentation, as it is the case -of Traffic Assignment, linked below. - -.. toctree:: - :maxdepth: 1 - - traffic_assignment \ No newline at end of file diff --git a/docs/source/modeling_with_aequilibrae.rst b/docs/source/modeling_with_aequilibrae.rst new file mode 100644 index 000000000..4b334ccd9 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae.rst @@ -0,0 +1,25 @@ +Modeling with AequilibraE +========================= + +AequilibraE is the first fully-featured Python package for transportation +modeling, and it aims to provide all the resources not easily available from +other open-source packages in the Python (NumPy, really) ecosystem. + +AequilibraE has also a fully features interface available as a plugin for the +open source software QGIS, which is separately maintained and discussed in +detail its `documentation `_. + +Contributions are welcome to the existing modules and/or in the form of new modules. + +In this section you can find a deep dive into modeling with AequilibraE, from +a start guide to a complete view into AequilibraE's data structure. + +.. toctree:: + :maxdepth: 1 + + modeling_with_aequilibrae/project + modeling_with_aequilibrae/modeling_concepts + modeling_with_aequilibrae/project_database + modeling_with_aequilibrae/parameter_file + modeling_with_aequilibrae/public_transport + diff --git a/docs/source/modeling_with_aequilibrae/create_model.rst b/docs/source/modeling_with_aequilibrae/create_model.rst new file mode 100644 index 000000000..a5b00f893 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/create_model.rst @@ -0,0 +1,360 @@ +.. _create_aequilibrae_model: + +======================== +Create AequilibraE model +======================== + +.. code-block:: python + import sys + from os.path import join + import numpy as np + from math import log10, floor + import matplotlib.pyplot as plt + from aequilibrae.distribution import GravityCalibration, Ipf, GravityApplication, SyntheticGravityModel + from aequilibrae import Parameters + from aequilibrae.project import Project + from aequilibrae.paths import PathResults, SkimResults + from aequilibrae.matrix import AequilibraeData, AequilibraeMatrix + from aequilibrae import logger + from aequilibrae.paths import TrafficAssignment, TrafficClass + + import logging + + ######### FILES AND FOLDER ######### + + fldr = 'D:/release/Sample models/sioux_falls_2020_02_15' + proj_name = 'SiouxFalls.sqlite' + + # remove the comments for the lines below to run the Chicago model example instead + # fldr = 'D:/release/Sample models/Chicago_2020_02_15' + + dt_fldr = '0_tntp_data' + prj_fldr = '1_project' + skm_fldr = '2_skim_results' + assg_fldr = '4_assignment_results' + dstr_fldr = '5_distribution_results' + frcst_fldr = '6_forecast' + ftr_fldr = '7_future_year_assignment' + + ########### LOGGING ################# + + p = Parameters() + p.parameters['system']['logging_directory'] = fldr + p.write_back() + # To make sure the logging will go where it should, stop the script here and + # re-run it + + # Because assignment takes a long time, we want the log to be shown here + stdout_handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter("%(asctime)s;%(name)s;%(levelname)s ; %(message)s") + stdout_handler.setFormatter(formatter) + logger.addHandler(stdout_handler) + + ########### PROJECT ################# + + project = Project() + project.load(join(fldr, prj_fldr)) + + ########### PATH COMPUTATION ################# + + # we build all graphs + project.network.build_graphs() + # We get warnings that several fields in the project are filled with NaNs. Which is true, but we won't + # use those fields + + # we grab the graph for cars + graph = project.network.graphs['c'] + + # let's say we want to minimize distance + graph.set_graph('distance') + + # And will skim time and distance while we are at it + graph.set_skimming(['free_flow_time', 'distance']) + + # And we will allow paths to be compute going through other centroids/centroid connectors + # required for the Sioux Falls network, as all nodes are centroids + graph.set_blocked_centroid_flows(False) + + # instantiate a path results object and prepare it to work with the graph + res = PathResults() + res.prepare(graph) + + # compute a path from node 2 to 13 + res.compute_path(2, 13) + + # We can get the sequence of nodes we traverse + res.path_nodes + + # We can get the link sequence we traverse + res.path + + # We can get the mileposts for our sequence of nodes + res.milepost + + # And We can the skims for our tree + res.skims + + # If we want to compute the path for a different destination and same origin, we can just do this + # It is way faster when you have large networks + res.update_trace(4) + + ########## SKIMMING ################### + + + # setup the object result + res = SkimResults() + res.prepare(graph) + + # And run the skimming + res.compute_skims() + + # The result is an AequilibraEMatrix object + skims = res.skims + + # We can export to AEM and OMX + skims.export(join(fldr, skm_fldr, 'skimming_on_time.aem')) + skims.export(join(fldr, skm_fldr, 'skimming_on_time.omx')) + + ######### TRAFFIC ASSIGNMENT WITH SKIMMING + + demand = AequilibraeMatrix() + demand.load(join(fldr, dt_fldr, 'demand.omx')) + demand.computational_view(['matrix']) # We will only assign one user class stored as 'matrix' inside the OMX file + + assig = TrafficAssignment() + + # Creates the assignment class + assigclass = TrafficClass(graph, demand) + + # The first thing to do is to add at list of traffic classes to be assigned + assig.set_classes([assigclass]) + + assig.set_vdf("BPR") # This is not case-sensitive # Then we set the volume delay function + + assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) # And its parameters + + assig.set_capacity_field("capacity") # The capacity and free flow travel times as they exist in the graph + assig.set_time_field("free_flow_time") + + # And the algorithm we want to use to assign + assig.set_algorithm('bfw') + + # since I haven't checked the parameters file, let's make sure convergence criteria is good + assig.max_iter = 1000 + assig.rgap_target = 0.00001 + + assig.execute() # we then execute the assignment + + # Convergence report is easy to see + import pandas as pd + convergence_report = pd.DataFrame(assig.assignment.convergence_report) + convergence_report.head() + + # The link flows are easy to export. + # we do so for csv and AequilibraEData + assigclass.results.save_to_disk(join(fldr, assg_fldr, 'link_flows_c.csv'), output="loads") + assigclass.results.save_to_disk(join(fldr, assg_fldr, 'link_flows_c.aed'), output="loads") + + # the skims are easy to get. + + # The blended one are here + avg_skims = assigclass.results.skims + + # The ones for the last iteration are here + last_skims = assigclass._aon_results.skims + + # Assembling a single final skim file can be done like this + # We will want only the time for the last iteration and the distance averaged out for all iterations + kwargs = {'file_name': join(fldr, assg_fldr, 'skims.aem'), + 'zones': graph.num_zones, + 'matrix_names': ['time_final', 'distance_blended']} + + # Create the matrix file + out_skims = AequilibraeMatrix() + out_skims.create_empty(**kwargs) + out_skims.index[:] = avg_skims.index[:] + + # Transfer the data + # The names of the skims are the name of the fields + out_skims.matrix['time_final'][:, :] = last_skims.matrix['free_flow_time'][:, :] + # It is CRITICAL to assign the matrix values using the [:,:] + out_skims.matrix['distance_blended'][:, :] = avg_skims.matrix['distance'][:, :] + + out_skims.matrices.flush() # Make sure that all data went to the disk + + # Export to OMX as well + out_skims.export(join(fldr, assg_fldr, 'skims.omx')) + + ############# TRIP DISTRIBUTION ################# + + # The demand is already in memory + + # Need the skims + imped = AequilibraeMatrix() + imped.load(join(fldr, assg_fldr, 'skims.aem')) + + # But before using the data, let's get some impedance for the intrazonals + # Let's assume it is 75% of the closest zone + + # If we run the code below more than once, we will be overwriting the diagonal values with non-sensical data + # so let's zero it first + np.fill_diagonal(imped.matrix['time_final'], 0) + + # We compute it with a little bit of NumPy magic + intrazonals = np.amin(imped.matrix['time_final'], where=imped.matrix['time_final'] > 0, + initial=imped.matrix['time_final'].max(), axis=1) + intrazonals *= 0.75 + + # Then we fill in the impedance matrix + np.fill_diagonal(imped.matrix['time_final'], intrazonals) + + # We set the matrices for use in computation + imped.computational_view(['time_final']) + demand.computational_view(['matrix']) + + + # Little function to plot TLFDs + def plot_tlfd(demand, skim, name): + # No science here. Just found it works well for Sioux Falls & Chicago + b = floor(log10(skim.shape[0]) * 10) + n, bins, patches = plt.hist(np.nan_to_num(skim.flatten(), 0), bins=b, + weights=np.nan_to_num(demand.flatten()), + density=False, facecolor='g', alpha=0.75) + + plt.xlabel('Trip length') + plt.ylabel('Probability') + plt.title('Trip-length frequency distribution') + plt.savefig(name, format="png") + plt.clf() + + + # Calibrate models with the two functional forms + for function in ['power', 'expo']: + model = GravityCalibration(matrix=demand, impedance=imped, function=function, nan_as_zero=True) + model.calibrate() + + # we save the model + model.model.save(join(fldr, dstr_fldr, f'{function}_model.mod')) + + # We save a trip length frequency distribution image + plot_tlfd(model.result_matrix.matrix_view, imped.matrix_view, + join(fldr, dstr_fldr, f'{function}_tfld.png')) + + # We can save the result of applying the model as well + # we can also save the calibration report + with open(join(fldr, dstr_fldr, f'{function}_convergence.log'), 'w') as otp: + for r in model.report: + otp.write(r + '\n') + + # We save a trip length frequency distribution image + plot_tlfd(demand.matrix_view, imped.matrix_view, join(fldr, dstr_fldr, 'demand_tfld.png')) + + ################ FORECAST ############################# + + # We compute the vectors from our matrix + mat = AequilibraeMatrix() + + mat.load(join(fldr, dt_fldr, 'demand.omx')) + mat.computational_view() + origins = np.sum(mat.matrix_view, axis=1) + destinations = np.sum(mat.matrix_view, axis=0) + + args = {'file_path':join(fldr, frcst_fldr, 'synthetic_future_vector.aed'), + "entries": mat.zones, + "field_names": ["origins", "destinations"], + "data_types": [np.float64, np.float64], + "memory_mode": False} + + vectors = AequilibraeData() + vectors.create_empty(**args) + + vectors.index[:] =mat.index[:] + + # Then grow them with some random growth between 0 and 10% - Plus balance them + vectors.origins[:] = origins * (1+ np.random.rand(vectors.entries)/10) + vectors.destinations[:] = destinations * (1+ np.random.rand(vectors.entries)/10) + vectors.destinations *= vectors.origins.sum()/vectors.destinations.sum() + + # Impedance matrix is already in memory + + # We want the main diagonal to be zero, as the original matrix does + # not have intrazonal trips + np.fill_diagonal(imped.matrix_view, np.nan) + + # Apply the gravity models + for function in ['power', 'expo']: + model = SyntheticGravityModel() + model.load(join(fldr, dstr_fldr, f'{function}_model.mod')) + + outmatrix = join(fldr,frcst_fldr, f'demand_{function}_model.aem') + apply = GravityApplication() + args = {"impedance": imped, + "rows": vectors, + "row_field": "origins", + "model": model, + "columns": vectors, + "column_field": "destinations", + "output": outmatrix, + "nan_as_zero":True + } + + gravity = GravityApplication(**args) + gravity.apply() + + #We get the output matrix and save it to OMX too + resm = AequilibraeMatrix() + resm.load(outmatrix) + resm.export(join(fldr,frcst_fldr, f'demand_{function}_model.omx')) + + # APPLY IPF + demand = AequilibraeMatrix() + demand.load(join(fldr, dt_fldr, 'demand.omx')) + demand.computational_view() + + args = {'matrix': demand, + 'rows': vectors, + 'columns': vectors, + 'column_field': "destinations", + 'row_field': "origins", + 'nan_as_zero': True} + + ipf = Ipf(**args) + ipf.fit() + + output = AequilibraeMatrix() + output.load(ipf.output.file_path) + + output.export(join(fldr,frcst_fldr, 'demand_ipf.aem')) + output.export(join(fldr,frcst_fldr, 'demand_ipf.omx')) + + + logger.info('\n\n\n TRAFFIC ASSIGNMENT FOR FUTURE YEAR') + + # Let's use the IPF matrix + demand = AequilibraeMatrix() + demand.load(join(fldr, frcst_fldr, 'demand_ipf.omx')) + demand.computational_view() # There is only one matrix there, so don;t even worry about its core name + + assig = TrafficAssignment() + + # Creates the assignment class + assigclass = TrafficClass(graph, demand) + + # The first thing to do is to add at list of traffic classes to be assigned + assig.set_classes([assigclass]) + + assig.set_vdf("BPR") # This is not case-sensitive # Then we set the volume delay function + + assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) # And its parameters + + assig.set_capacity_field("capacity") # The capacity and free flow travel times as they exist in the graph + assig.set_time_field("free_flow_time") + + # And the algorithm we want to use to assign + assig.set_algorithm('bfw') + + # since I haven't checked the parameters file, let's make sure convergence criteria is good + assig.max_iter = 1000 + assig.rgap_target = 0.00001 + + assig.execute() # we then execute the assignment \ No newline at end of file diff --git a/docs/source/modeling_with_aequilibrae/modeling_concepts.rst b/docs/source/modeling_with_aequilibrae/modeling_concepts.rst new file mode 100644 index 000000000..67c5c3082 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/modeling_concepts.rst @@ -0,0 +1,25 @@ +.. _modeling_concepts: + +Modeling Concepts +================= + +Modeling with AequilibraE is not dissimilar than modeling with traditional commercial packages, +as we strive to make it as easy as possible for seasoned modelers to migrate their models and +workflows to AequilibraE. + +Although modeling with AequilibraE should feel somewhat familiar to seasoned modelers, especially +those used to programming, the mechanics of some of AequilibraE procedures might be foreign to +some of users, so this section of the documentation will include discussions of the mechanics +of some of these procedures and some light discussion on its motivation. + +Further, many AequilibraE users are new to the *craft*, so we have elected to start +creating documentation on the most important topics in the transportation modeling practice, where +we detail how these concepts are translated into the AequilibraE tools and recommended workflows. + + +.. toctree:: + :maxdepth: 1 + + modeling_concepts/multi_class_equilibrium + modeling_concepts/assignment_mechanics + diff --git a/docs/source/modeling_with_aequilibrae/modeling_concepts/assignment_mechanics.rst b/docs/source/modeling_with_aequilibrae/modeling_concepts/assignment_mechanics.rst new file mode 100644 index 000000000..1e072598a --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/modeling_concepts/assignment_mechanics.rst @@ -0,0 +1,335 @@ +Path-finding and assignment mechanics +------------------------------------- + +Performing traffic assignment, or even just computing paths through a network is +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 +for a heavy truck, as it happens in practice. + +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 +from networks that are :ref:`NOT housed inside an AequilibraE model `. + +.. _aequilibrae-graphs: + +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 +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 +***network** and the **graph**. + +.. code-block:: python + + from aequilibrae.paths import Graph + + g = Graph() + + # g.network + # g.graph + +Directionality +^^^^^^^^^^^^^^ + +Links in the Network table (the Pandas representation of the project's *Links* table) are +potentially bi-directional, and the directions allowed for traversal are dictated by the +field *direction*, where -1 and 1 denote only BA and AB traversal respectively and 0 denotes +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 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 +the graph used to set computations (e.g. field to minimize during path-finding, skimming, +etc.) will be **free_flow_travel_time**. + +Graphs from a model +^^^^^^^^^^^^^^^^^^^ + +Building graphs directly from an AequilibraE model is the easiest option for beginners +or when using AequilibraE in anger, as much of the setup is done by default. + +.. code-block:: python + + from aequilibrae import Project + + 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 + +Manipulating graphs in memory +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As mentioned before, the AequilibraE Graph can be manipulated in memory, with all its +components available for editing. One of the simple tools available directly in the +API is a method call for excluding one or more links from the Graph, **which is done** +**in place**. + +.. code-block:: python + + 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 + + graph.prepare_graph(graph.centroids) + if graph.skim_fields: + graph.set_skimming(graph.skim_fields) + +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 +of fields that can be skimmed. + + +.. code-block:: python + + graph.set_skimming(["tolls", "distance", "free_flow_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. +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". + +.. code-block:: python + + graph.prepare_graph(np.array([13, 169, 2197, 28561, 371293], np.int)) + graph.set_blocked_centroid_flows(False) + +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 introduces on version 0.6.1 of the +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:`example_usage_parameters` . + +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() + +Volume Delay Function +^^^^^^^^^^^^^^^^^^^^^ + +For now, the only VDF functions available in AequilibraE are the + +* BPR [8]_ + +.. math:: CongestedTime_{i} = FreeFlowTime_{i} * (1 + \alpha * (\frac{Volume_{i}}{Capacity_{i}})^\beta) + +* Spiess' conical [7]_ + +.. 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. + +.. _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 + +Example: + +.. code-block:: python + + tc = TrafficClass("car", graph_car, matrix_car) + + tc2 = TrafficClass("truck", graph_truck, matrix_truck) + +* **pce** - The passenger-car equivalent is the standard way of modeling + multi-class traffic assignment equilibrium in a consistent manner (see [4]_ 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 + + tc2.set_pce(2.5) + tc2.set_fixed_cost("truck_toll") + tc2.set_vot(0.35) + +To add traffic classes to the assignment instance it is just a matter of making +a method call: + +.. code-block:: python + + assig.set_classes([tc, tc2]) + +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**. + +To choose a field that exists in the graph, we just pass the parameters as +follows: + +.. code-block:: python + + assig.set_vdf_parameters({"alpha": "alphas", "beta": "betas"}) + + +To pass global values, it is simply a matter of doing the following: + +.. code-block:: python + + assig.set_vdf_parameters({"alpha": 0.15, "beta": 4}) + + +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) + +Finally, one can execute assignment: + +.. code-block:: python + + assig.execute() + +:ref:`convergence_criteria` is discussed in a different section. + diff --git a/docs/source/modeling_with_aequilibrae/modeling_concepts/multi_class_equilibrium.rst b/docs/source/modeling_with_aequilibrae/modeling_concepts/multi_class_equilibrium.rst new file mode 100644 index 000000000..ae9d673c5 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/modeling_concepts/multi_class_equilibrium.rst @@ -0,0 +1,207 @@ +.. _multiclass_equilibrium: + +Multi-class Equilibrium assignment +---------------------------------- + +While single-class equilibrium traffic assignment [1]_ is mathematically simple, +multi-class traffic assignment [9]_, especially when including monetary costs +(e.g. tolls) and multiple classes with different Passenger Car Equivalent (PCE) +factors, requires more sophisticated mathematics. + +As it is to be expected, strict convergence of multi-class equilibrium assignments +comes at the cost of specific technical requirements and more advanced equilibration +algorithms have slightly different requirements. + +Cost function +~~~~~~~~~~~~~ + +AequilibraE supports class-=specific cost functions, where each class can include +the following: + +* PCE +* Link-based fixed financial cost components +* Value-of-Time (VoT) + +.. _technical_requirements_multi_class: + +Technical requirements +~~~~~~~~~~~~~~~~~~~~~~ + +This documentation is not intended to discuss in detail the mathematical +requirements of multi-class traffic assignment, which can be found discussed in +detail on [4]_. + +A few requirements, however, need to be made clear. + +* All traffic classes shall have identical free-flow travel times throughout the + network + +* Each class shall have an unique Passenger Car Equivalency (PCE) factor for all links + +* Volume delay functions shall be monotonically increasing. *Well behaved* + functions are always something we are after + +For the conjugate and Biconjugate Frank-Wolfe algorithms it is also necessary +that the VDFs are differentiable. + +.. _convergence_criteria: + +Convergence criteria +~~~~~~~~~~~~~~~~~~~~ + +Convergence in AequilibraE is measured solely in terms of relative gap, which is +a somewhat old recommendation [5]_, but it is still the most used measure in +practice, and is detailed below. + +.. math:: RelGap = \frac{\sum_{a}V_{a}^{*}*C_{a} - \sum_{a}V_{a}^{AoN}*C_{a}}{\sum_{a}V_{a}^{*}*C_{a}} + +The algorithm's two stop criteria currently used are the maximum number of +iterations and the target Relative Gap, as specified above. These two parameters +are described in detail in the :ref:`parameters_assignment` section, in the +:ref:`parameters_file`. + +Algorithms available +~~~~~~~~~~~~~~~~~~~~ + +All algorithms have been implemented as a single software class, as the +differences between them are simply the step direction and step size after each +iteration of all-or-nothing assignment, as shown in the table below + ++-------------------------------+-----------------------------------------------------------+-------------------------------------------------+ +| Algorithm | Step direction | Step Size | ++===============================+===========================================================+=================================================+ +| Method of Successive Averages | All-or-Nothing assignment (AoN) | function of the iteration number | ++-------------------------------+-----------------------------------------------------------+-------------------------------------------------+ +| Frank-Wolfe | All-or-Nothing assignment | Optimal value derived from Wardrop's principle | ++-------------------------------+-----------------------------------------------------------+-------------------------------------------------+ +| Conjugate Frank-Wolfe | Conjugate direction (Current and previous AoN) | Optimal value derived from Wardrop's principle | ++-------------------------------+-----------------------------------------------------------+-------------------------------------------------+ +| Biconjugate Frank-Wolfe | Biconjugate direction (Current and two previous AoN) | Optimal value derived from Wardrop's principle | ++-------------------------------+-----------------------------------------------------------+-------------------------------------------------+ + +.. note:: + Our implementations of the conjudate and Biconjugate-Frank-Wolfe methods + should be inherently proportional [6]_, but we have not yet carried the + appropriate testing that would be required for an empirical proof. + +Method of Successive Averages (MSA) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This algorithm has been included largely for historical reasons, and we see very +little reason to use it. Yet, it has been implemented with the appropriate +computation of relative gap computation and supports all the analysis features +available. + +Frank-Wolfe (FW) +^^^^^^^^^^^^^^^^ + +The implementation of Frank-Wolfe in AequilibraE is extremely simple from an +implementation point of view, as we use a generic optimizer from SciPy as an +engine for the line search, and it is a standard implementation of the algorithm +introduced by LeBlanc in 1975 [2]_. + + +Conjugate Frank-Wolfe +^^^^^^^^^^^^^^^^^^^^^ + +The conjugate direction algorithm was introduced in 2013 [3]_, which is quite +recent if you consider that the Frank-Wolfe algorithm was first applied in the +early 1970's, and it was introduced at the same as its Biconjugate evolution, +so it was born outdated. + +Biconjugate Frank-Wolfe +^^^^^^^^^^^^^^^^^^^^^^^ + +The Biconjugate Frank-Wolfe algorithm is currently the fastest converging link- +based traffic assignment algorithm used in practice, and it is the recommended +algorithm for AequilibraE users. Due to its need for previous iteration data, +it **requires more memory** during runtime, but very large networks should still +fit nicely in systems with 16Gb of RAM. + +Implementation details & tricks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A few implementation details and tricks are worth mentioning not because they are +needed to use the software, but because they were things we grappled with during +implementation, and it would be a shame not register it for those looking to +implement their own variations of this algorithm or to slight change it for +their own purposes. + +* The relative gap is computed with the cost used to compute the All-or-Nothing + portion of the iteration, and although the literature on this is obvious, we + took some time to realize that we should re-compute the travel costs only + **AFTER** checking for convergence. + +* In some instances, Frank-Wolfe is extremely unstable during the first + iterations on assignment, resulting on numerical errors on our line search. + We found that setting the step size to the corresponding MSA value (1/ + current iteration) resulted in the problem quickly becoming stable and moving + towards a state where the line search started working properly. This technique + was generalized to the conjugate and biconjugate Frank-Wolfe algorithms. + +Multi-threaded implementation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +AequilibraE's All-or-Nothing assignment (the basis of all the other algorithms) +has been parallelized in Python using the threading library, which is possible +due to the work we have done with memory management to release Python's Global +Interpreter Lock. +Other opportunities for parallelization, such as the computation of costs and +its derivatives (required during the line-search optimization step), as well as +all linear combination operations for vectors and matrices have been achieved +through the use of OpenMP in pure Cython code. These implementations can be +cound on a file called *parallel_numpy.pyx* if you are curious to look at. + +Much of the gains of going back to Cython to parallelize these functions came +from making in-place computation using previously existing arrays, as the +instantiation of large NumPy arrays can be computationally expensive. + +.. _traffic-assignment-references: + + +Handling the network +~~~~~~~~~~~~~~~~~~~~ +The other important topic when dealing with multi-class assignment is to have +a single consistent handling of networks, as in the end there is only physical +network across all modes, regardless of access differences to each mode (e.g. truck +lanes, High-Occupancy Lanes, etc.). This handling is often done with something +called a **super-network**. + +Super-network +^^^^^^^^^^^^^ +We deal with a super-network by having all classes with the same links in their +sub-graphs, but assigning *b_node* identical to *a_node* for all links whenever a +link is not available for a certain user class. +This approach is slightly less efficient when we are computing shortest paths, but +it gets eliminated when topologically compressing the network for centroid-to-centroid +path computation and it is a LOT more efficient when we are aggregating flows. + +The use of the AequilibraE project and its built-in methods to build graphs +ensure that all graphs will be built in a consistent manner and multi-class +assignment is possible. + +References +~~~~~~~~~~ + +Traffic assignment and equilibrium +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. [1] Wardrop J. G. (1952) "Some theoretical aspects of road traffic research."Proceedings of the Institution of Civil Engineers 1952, 1(3):325-362. Available in: https://www.icevirtuallibrary.com/doi/abs/10.1680/ipeds.1952.11259 + +.. [2] LeBlanc L. J., Morlok E. K. and Pierskalla W. P. (1975) "An efficient approach to solving the road network equilibrium traffic assignment problem". Transportation Research, 9(5):309-318. Available in: https://doi.org/10.1016/0041-1647(75)90030-1 + +.. [3] Mitradjieva, M. and Lindberg, P.O. (2013) "The Stiff Is Moving—Conjugate Direction Frank-Wolfe Methods with Applications to Traffic Assignment". Transportation Science, 47(2):280-293. Available in: https://doi.org/10.1287/trsc.1120.0409 + +.. [4] 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 + +.. [5] Rose, G., Daskin, M., Koppelman, F. (1988) "An examination of convergence error in equilibrium traffic assignment models", Transportation Res. B, 22(4):261-274. Available in: https://doi.org/10.1016/0191-2615(88)90003-3 + +.. [6] Florian, M., Morosan, C.D. (2014) "On uniqueness and proportionality in multi-class equilibrium assignment", Transportation Research Part B, 70:261-274. Available in: https://doi.org/10.1016/j.trb.2014.06.011 + +.. [9] Marcotte, P., Patriksson, M. (2007) "Chapter 10 Traffic Equilibrium - Handbooks in Operations Research and Management Science, Vol 14", Elsevier. Editors Barnhart, C., Laporte, G. https://doi.org/10.1016/S0927-0507(06)14010-4 + +Volume delay functions +^^^^^^^^^^^^^^^^^^^^^^ + +.. [7] 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 + +.. [8] Hampton Roads Transportation Planning Organization, Regional Travel Demand Model V2 (2020). Available in: https://www.hrtpo.org/uploads/docs/2020_HamptonRoads_Modelv2_MethodologyReport.pdf \ No newline at end of file diff --git a/docs/source/parameter_file.rst b/docs/source/modeling_with_aequilibrae/parameter_file.rst similarity index 79% rename from docs/source/parameter_file.rst rename to docs/source/modeling_with_aequilibrae/parameter_file.rst index fab869030..1670b7acd 100644 --- a/docs/source/parameter_file.rst +++ b/docs/source/modeling_with_aequilibrae/parameter_file.rst @@ -1,87 +1,32 @@ .. _parameters_file: -Parameter File -============== +Parameters YAML File +==================== -The parameter file has 4 distinct sections, each of which hold parameters for a -certain portion of the software. - -* :ref:`parameters_system` +The parameter file holds the parameters information for a certain portion of the software. * :ref:`parameters_assignment` - -* :ref:`parameters_osm` - * :ref:`parameters_distribution` - * :ref:`parameters_network` - -Basic use of the parameters module is exemplified through the AequilibraE API -as detailed in the :ref:`example_usage_parameters` section of the use cases. - -.. _parameters_system: - -System -------- - -The system section of the parameters file holds information on things like the -number of threads used in multi-threaded processes, logging and temp folders -and whether we should be saving information to a log file at all, as exemplified -below. - -.. image:: images/parameters_system_example.png - :width: 812 - :align: center - :alt: System example - -The number of CPUs have a special behaviour defined, as follows: - -* **cpus<0** : The system will use the total number logical processors - **MINUS** the absolute value of **cpus** - -* **cpus=0** : The system will use the total number logical processors available - -* **cpus>0** : The system will use exactly **cpus** for computation, limited to - the total number logical processors available - -A few of these parameters, however, are targeted at its QGIS plugin, which is -the case of the *driving side* and *default_directory* parameters. - -.. _parameters_osm: - -Open Streeet Maps ------------------ -The OSM section of the parameter file is relevant only when one plans to -download a substantial amount of data from an Overpass API, in which case it is -recommended to deploy a local Overpass server. - -.. image:: images/parameters_assignment_example.png - :width: 840 - :align: center - :alt: OSM example - -The user is also welcome to change the maximum area for a single query to the -Overpass API (m\ :sup:`2`) and the pause duration between successive -requests *sleeptime*. - -It is also possible to set a custom address for the Nominatim server, but its -use by AequilibraE is so small that it is likely not necessary to do so. +* :ref:`parameters_system` +* :ref:`parameters_osm` .. _parameters_assignment: Assignment ---------- + The assignment section of the parameter file is the smallest one, and it -contains only the convergence criteria for assignment in terms of maximum number +contains only the convergence criteria for assignment in terms of the maximum number of iterations and target Relative Gap. -.. image:: images/parameters_assignment_example.png +.. image:: ../images/parameters_assignment_example.png :width: 487 :align: center :alt: Assignment example Although these parameters are required to exist in the parameters file, one can -override them during assignment, as detailed in :ref:`convergence_criteria`. +override them during the assignment, as detailed in :ref:`convergence_criteria`. .. _parameters_distribution: @@ -94,7 +39,7 @@ contains only the parameters for number of maximum iterations, convergence level and maximum trip length to be applied in Iterative Proportional Fitting and synthetic gravity models, as shown below. -.. image:: images/parameters_distribution_example.png +.. image:: ../images/parameters_distribution_example.png :width: 546 :align: center :alt: Distribution example @@ -104,12 +49,13 @@ synthetic gravity models, as shown below. Network ------- -There are three groups of parameters under the network section: *links*, *nodes* & *OSM*. The -first are basically responsible for the design of the network to be created in case a new -project/network is to bre created from scratch, and for now each one of these groups -contains only a single group of parameters called *fields*. +There are four groups of parameters under the network section: *links*, *nodes*, +*OSM*, and *GMNS*. The first are basically responsible for the design of the network +to be created in case a new project/network is to bre created from scratch, and for +now each one of these groups contains only a single group of parameters called +*fields*. -link fields +Link Fields ~~~~~~~~~~~ The section for link fields are divided into *one-way* fields and *two-way* fields, where the @@ -127,7 +73,7 @@ The data types available are those that exist within the `SQLite specification `_ . We recommend limiting yourself to the use of **integer**, **numeric** and **varchar**. -.. image:: images/parameters_links_example.png +.. image:: ../images/parameters_links_example.png :width: 704 :align: center :alt: Link example @@ -152,7 +98,7 @@ forward/backward values tagged). For this reason, one can use the parameter *osm to define what to do with numeric tag values that have not been tagged for both directions. the allowed values for this parameter are **copy** and **divide**, as shown below. -.. image:: images/parameters_links_osm_behaviour.png +.. image:: ../images/parameters_links_osm_behaviour.png :width: 437 :align: center :alt: OSM behaviour examples @@ -172,11 +118,10 @@ would have no effect here. Open Street Maps ----------------- -The **OSM** group of parameters has as its only -there are further groups: **modes** and **all_link_types**. +~~~~~~~~~~~~~~~~ +The **OSM** group of parameters has two specifications: **modes** and **all_link_types**. -List of key tags we will import for each mode. Description of tags can be found on +**modes** contains the list of key tags we will import for each mode. Description of tags can be found on `Open-Street Maps `_, and we recommend not changing the standard parameters unless you are exactly sure of what you are doing. @@ -188,3 +133,67 @@ This feature is stored under the key *mode_filter* under each mode to be importe There is also the possibility that not all keywords for link types for the region being imported, and therefore unknown link type tags are treated as a special case for each mode, and that is controlled by the key *unknown_tags* in the parameters file. + +GMNS +~~~~ + +The **GMNS** group of parameters has four specifications: **critical_dist**, **link**, +**node**, and **use_definition**. + +.. image:: ../images/parameter_yaml_files_gmns.png + :align: center + :alt: GMNS parameter group +| +**critical_dist** is a numeric threshold for the distance. + +Under the keys **links**, **nodes**, and **use_definition** there are the fields +*equivalency* and *fields*. They represent the equivalency between GMNS and +AequilibraE data fields and data types for each field. + +.. _parameters_system: + +System +------ + +The system section of the parameters file holds information on the +number of threads used in multi-threaded processes, logging and temp folders +and whether we should be saving information to a log file at all, as exemplified +below. + +.. image:: ../images/parameters_system_example.png + :width: 812 + :align: center + :alt: System example + +The number of CPUs have a special behaviour defined, as follows: + +* **cpus<0** : The system will use the total number logical processors + **MINUS** the absolute value of **cpus** + +* **cpus=0** : The system will use the total number logical processors available + +* **cpus>0** : The system will use exactly **cpus** for computation, limited to + the total number logical processors available + +A few of these parameters, however, are targeted at its QGIS plugin, which is +the case of the *driving side* and *default_directory* parameters. + +.. _parameters_osm: + +Open Streeet Maps +----------------- +The OSM section of the parameter file is relevant only when one plans to +download a substantial amount of data from an Overpass API, in which case it is +recommended to deploy a local Overpass server. + +.. image:: ../images/parameters_osm_example.png + :width: 840 + :align: center + :alt: OSM example + +The user is also welcome to change the maximum area for a single query to the +Overpass API (m\ :sup:`2`) and the pause duration between successive +requests *sleeptime*. + +It is also possible to set a custom address for the Nominatim server, but its +use by AequilibraE is so small that it is likely not necessary to do so. diff --git a/docs/source/modeling_with_aequilibrae/project.rst b/docs/source/modeling_with_aequilibrae/project.rst new file mode 100644 index 000000000..0ede64517 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project.rst @@ -0,0 +1,98 @@ +.. _project: + +The AequilibraE project +----------------------- + +Similarly to commercial packages, any AequilibraE project must have a certain +structure and follow a certain set of guidelines in order for software to +work correctly. + +One of these requirements is that AequilibraE currently only supports one +projection system for all its layers, which is the **EPSG:4326** (WGS84). +This limitation is planned to be lifted at some point, but it does not impact +the result of any modeling procedure. + +AequilibraE is built on the shoulder of much older and more established +projects, such as `SQLite `_, +`SpatiaLite `_ and `NumPy +`_, as well as reasonably new industry standards such as the +`Open-Matrix format `_. + +impressive performance, portability, self containment and open-source character +of these pieces of software, along with their large user base and wide +industry support make them solid options to be AequilibraE's data backend. + +Since working with Spatialite is not just a matter of a *pip install*, +please refer to :ref:`dependencies`. For QGIS users this is not a concern, while +for Windows users this dependency is automatically handled under the hood, but +the details are also discussed in the aforementioned dependencies section. + +Project structure +~~~~~~~~~~~~~~~~~ + +Since version 0.7, the AequilibraE project consists of a main folder, where a +series of files and sub folders exist, and the current project organization +is as follows: + +.. image:: ../images/project_structure.png + :width: 700 + :alt: AequilibraE project structure + +| + +The main component of an AequilibraE model is the **project_database.sqlite**, +where the network and zoning system are stored and maintained, as well as the +documentation records of all matrices and procedure results stored in other +folders and databases. + +The second key component of any model is the **parameters.yaml** file, which +holds the default values for a number of procedures (e.g. assignment +convergence), as well as the specification for networks imported from +Open-Street Maps and other general *import-export* parameters. + +The third and last required component of an AequilibraE model is the Matrices +folder, where all the matrices in binary format (in AequilibraE's native AEM or +OMX formats) should be placed. This folder can be empty, however, as no +particular matrix is required to exist in an AequilibraE model. + +The database that stores results in tabular format (e.g. link loads from traffic +assignment), **results_database.sqlite** is created on-the-fly the first time +a command to save a tabular result into the model is invoked, so the user does +not need to worry about its existence until it is automatically created. + +The **demand_database.sqlite** is envisioned to hold all the demand-related +information, and it is not yet structured within the AequilibraE code, as there +is no pre-defined demand model available for use with AequilibraE. This detabase +is not created with the model, but we recommend using this concept on +your demand models. + +The **public_transport.sqlite** database holds a *transportation route system* for +a model, and has been introduced in AequilibraE version 0.9. This database is +also created *on-the-fly* when the user imports a GTFS source into an AequilibraE +model, but there is still no support for manually or programmatically adding routes +to a route system as of yet. + +Package components: A conceptual view +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As all the components of an AequilibraE model based on open-source software and +open-data standards, modeling with AequilibraE is a little different than +modeling with commercial packages, as the user can read and manipulate model +components outside the software modeling environments (Python and QGIS). + +Thus, using/manipulating each one of an AequilibraE model components can be done +in different ways depending on the tool you use for such. + +It is then important to highlight that AequilibraE, as a software, is divided in +three very distinctive layers. The first, which is responsible for tables +consistent with each other (including links and nodes, modes and link_types), +are embedded in the data layer in the form of geo-spatial database triggers. The +second is the Python API, which provides all of AequilibraE's core algorithms +and data manipulation facilities. The third is the GUI implemented in QGIS, +which provides a user-friendly interface to access the model, visualize results +and run procedures. + +These software layers are *stacked* and depend on each other, which means that any +network editing done in SQLite, Python or QGIS will go through the SpatiaLite triggers, +while any procedure such as traffic assignment done in QGIS is nothing more than an +API call to the corresponding Python method. diff --git a/docs/source/modeling_with_aequilibrae/project_database.rst b/docs/source/modeling_with_aequilibrae/project_database.rst new file mode 100644 index 000000000..c33e89a37 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project_database.rst @@ -0,0 +1,27 @@ + +Project database +---------------- +More details on the **project_database.sqlite** are discussed on a nearly *per-table* +basis below, and we recommend understanding the role of each table before setting +an AequilibraE model you intend to use in anger. + +.. toctree:: + :maxdepth: 1 + + project_database/about + project_database/network + project_database/modes + project_database/link_types + project_database/matrices + project_database/zones + project_database/parameters_metadata + project_database/results + + +A more technical view of the database structure, including the SQL queries used to +create each table and the indices used for each table are also available. + +.. toctree:: + :maxdepth: 1 + + project_database/data_model/datamodel.rst diff --git a/docs/source/modeling_with_aequilibrae/project_database/about.rst b/docs/source/modeling_with_aequilibrae/project_database/about.rst new file mode 100644 index 000000000..9f8f66b37 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project_database/about.rst @@ -0,0 +1,26 @@ +.. _tables_about: + +About table +~~~~~~~~~~~ + +The **about** table is the simplest of all tables in the AequilibraE project, +but it is the one table that contains the documentation about the project, and +it is therefore crucial for project management and quality assurance during +modeling projects. + +It is possible to create new information fields programmatically. Once +the new field is added, the underlying database is altered and the field will +be present when the project is open during future use. + +This table, which can look something like the example from image below, is required +to exist in AequilibraE but it is not currently actively used by any process but +we strongly recommend not to edit the information on **projection** and +**aequilibrae_version**, as these are fields that might or might not be used by +the software to produce valuable information to the user with regards to +opportunities for version upgrades. + +.. image:: ../../images/about_table_example.png + :width: 800 + :alt: About table structure + +An API for editing the contents of this database is available from the API documentation. \ No newline at end of file diff --git a/docs/source/modeling_with_aequilibrae/project_database/datamodel.rst.template b/docs/source/modeling_with_aequilibrae/project_database/datamodel.rst.template new file mode 100644 index 000000000..b3980a814 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project_database/datamodel.rst.template @@ -0,0 +1,30 @@ +.. _supply_data_model: + +SQL Data model +^^^^^^^^^^^^^^ + +The data model presented in this section pertains only to the structure of +AequilibraE's project_database and general information about the usefulness +of specific fields, especially on the interdependency between tables. + +Conventions +''''''''''' + +A few conventions have been adopted in the definition of the data model and some +are listed below: + +- Geometry field is always called **geometry** +- Projection is 4326 (WGS84) +- Tables are all in all lower case + + +.. Do not touch below this line unless you know EXACTLY what you are doing. +.. it will be automatically populated + +Project tables +'''''''''''''' + +.. toctree:: + :maxdepth: 1 + + LIST_OF_TABLES \ No newline at end of file diff --git a/docs/source/modeling_with_aequilibrae/project_database/exporting_to_gmns.rst b/docs/source/modeling_with_aequilibrae/project_database/exporting_to_gmns.rst new file mode 100644 index 000000000..ba3fbdd20 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project_database/exporting_to_gmns.rst @@ -0,0 +1,37 @@ +.. _exporting_to_gmns: + +Exporting AequilibraE model to GMNS format +========================================== + +After loading an existing AequilibraE project, you can export it to GMNS format. + +.. image:: ../../images/plot_export_to_gmns.png + :align: center + :alt: example + :target: _auto_examples/plot_export_to_gmns.html + +| +As of July 2022, it is possible to export an AequilibraE network to the following +tables in GMNS format: + +* link table +* node table +* use_definition table + +This list does not include the optional use_group table, which is an optional argument +of the ``create_from_gmns()`` function, because mode groups are not used in the +AequilibraE modes table. + +In addition to all GMNS required fields for each of the three exported tables, some +other fields are also added as riminder of where the features came from when looking +back at the AequilibraE project. + +.. note:: + + **When a node is identified as a centroid in the AequilibraE nodes table, this** + **information is transmitted to the GMNS node table by means of the field** + **'node_type', which is set to 'centroid' in this case. The 'node_type' field** + **is an optinal field listed in the GMNS node table specification.** + +You can find the GMNS specification +`here `_. diff --git a/docs/source/modeling_with_aequilibrae/project_database/importing_from_gmns.rst b/docs/source/modeling_with_aequilibrae/project_database/importing_from_gmns.rst new file mode 100644 index 000000000..2af1707cb --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project_database/importing_from_gmns.rst @@ -0,0 +1,41 @@ +.. _importing_from_gmns: + +Importing from files in GMNS format +=================================== + +Before importing a network from a source in GMNS format, it is imperative to know +in which spatial reference its geometries (links and nodes) were created. If the SRID +is different than 4326, it must be passed as an input using the argument 'srid'. + +.. image:: ../../images/plot_import_from_gmns.png + :align: center + :alt: example + :target: _auto_examples/plot_import_from_gmns.html + +| +As of July 2022, it is possible to import the following files from a GMNS source: + +* link table; +* node table; +* use_group table; +* geometry table. + +You can find the specification for all these tables in the GMNS documentation, +`here `_. + +By default, the method ``create_from_gmns()`` read all required and optional fields +specified in the GMNS link and node tables specification. If you need it to read +any additional fields as well, you have to modify the AequilibraE parameters as +shown in the :ref:`example `. +When adding a new field to be read in the parameters.yml file, it is important to +keep the "required" key set to False, since you will always be adding a non-required +field. Required fields for a specific table are only those defined in the GMNS +specification. + +.. note:: + + **In the AequilibraE nodes table, if a node is to be identified as a centroid, its** + **'is_centroid' field has to be set to 1. However, this is not part of the GMNS** + **specification. Thus, if you want a node to be identified as a centroid during the** + **import process, in the GMNS node table you have to set the field 'node_type' equals** + **to 'centroid'.** \ No newline at end of file diff --git a/docs/source/modeling_with_aequilibrae/project_database/importing_from_osm.rst b/docs/source/modeling_with_aequilibrae/project_database/importing_from_osm.rst new file mode 100644 index 000000000..38dcfd10a --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project_database/importing_from_osm.rst @@ -0,0 +1,74 @@ +.. _importing_from_osm: + +Importing from Open Street Maps +=============================== + +Please review the information :ref:`parameters_osm` + +.. note:: + + **ALL links that cannot be imported due to errors in the SQL insert** + **statements are written to the log file with error message AND the SQL** + **statement itself, and therefore errors in import can be analyzed for** + **re-downloading or fixed by re-running the failed SQL statements after** + **manual fixing** + +.. _sqlite_python_limitations: + +Python limitations +------------------ +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 + :width: 1383 + :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 +platform that does not support both RTree and Spatialite. diff --git a/docs/source/project_docs/link_types.rst b/docs/source/modeling_with_aequilibrae/project_database/link_types.rst similarity index 69% rename from docs/source/project_docs/link_types.rst rename to docs/source/modeling_with_aequilibrae/project_database/link_types.rst index 8d9dc7278..4b8b13642 100644 --- a/docs/source/project_docs/link_types.rst +++ b/docs/source/modeling_with_aequilibrae/project_database/link_types.rst @@ -1,30 +1,17 @@ .. _tables_link_types: -================ Link types table -================ +~~~~~~~~~~~~~~~~ The **link_types** table exists to list all the link types available in the model's network, and its main role is to support processes such as adding centroids and centroid connectors and to store reference data like default lane capacity for each link type. -.. _basic_fields: - -Basic fields ------------- - -The modes table has five main fields, being the *link_type*, *link_type_id*, -*description*, *lanes* and *lane_capacity*. Of these fields, the only mandatory -ones are *link_type* and *link_type_id*, where the former appears in the -link_table on the field *link_type*, while the latter is a single character that -can be concatenated into the *nodes*** layer to identify the link_types that -connect into each node. - .. _reserved_values: Reserved values ---------------- +^^^^^^^^^^^^^^^ There are two default link types in the link_types table and that cannot be removed from the model without breaking it. @@ -34,34 +21,37 @@ removed from the model without breaking it. - **default** - This link type exists to facilitate the creation of networks when link types are irrelevant. The identifying letter for this mode is **y**. - That is right, you have from a to x to create your own link types. :-D + That is right, you have from a to x to create your own link types, as well + as all upper-case letters of the alphabet. .. _adding_new_link_types: -Adding new link_types to an existing project --------------------------------------------- +Adding new link_types to a project +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To manually add link types, the user can add further link types to the -parameters file, as shown below. +Adding link types to a project can be done through the Python API or directly into +the link_types table, which could look like the following. +.. image:: ../../images/link_types_table.png + :width: 800 + :alt: Link_types table structure -Adding new link_types to a project ----------------------------------- -**STILL NEED TO BUILD THE API FOR SUCH** +.. note:: + + **Both link_type and link_type_id MUST be unique** .. _consistency_triggers: Consistency triggers --------------------- +^^^^^^^^^^^^^^^^^^^^ As it happens with the links and nodes tables, (:ref:`network_triggers_behaviour`), the link_types table is kept consistent with the links table through the use of database triggers - .. _change_reserved_types: Changes to reserved link_types -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +'''''''''''''''''''''''''''''' For both link types mentioned about (**y** & **z**), changes to the *link_type* and *link_type_id* fields, as well as the removal of any of these records are @@ -71,7 +61,7 @@ physical link type and one virtual link type present in the model. .. _change_link_type_for_link: Changing the link_type for a certain link -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +''''''''''''''''''''''''''''''''''''''''' Whenever we change the link_type associated to a link, we need to check whether that link type exists in the links_table. @@ -79,36 +69,37 @@ that link type exists in the links_table. This condition is ensured by specific trigger checking whether the new link_type exists in the link table. If if it does not, the transaction will fail. -We also need to update the **modes** field the nodes connected to the link with -a new string of all the different link type IDs connected to them. +We also need to update the **link_types** field the nodes connected to the link +with a new string of all the different **link_type_id**s connected to them. .. _adding_new_link: Adding a new link -^^^^^^^^^^^^^^^^^ +''''''''''''''''' The exact same behaviour as for :ref:`change_link_type_for_link` applies in this case, but it requires an specific trigger on the **creation** of the link. .. _editing_lt_on_lt_table: Editing a link_type in the link_types table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Whenever we want to edit a link_type in the link_types table, we need to check for -two conditions: +''''''''''''''''''''''''''''''''''''''''''' +Whenever we want to edit a link_type in the link_types table, we need to check +for two conditions: * The new link_type_id is exactly one character long -* The old link_type is not still in use on the network +* The old link_type is not in use on the network For each condition, a specific trigger was built, and if any of the checks fails, the transaction will fail. The requirements for uniqueness and non-absent values are guaranteed during the -construction of the modes table by using the keys **UNIQUE** and **NOT NULL**. +construction of the link_types table by using the keys **UNIQUE** and +**NOT NULL**. .. _adding_new_ltype: Adding a new link_type to the link_types table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +'''''''''''''''''''''''''''''''''''''''''''''' In this case, only the first behaviour mentioned above on :ref:`editing_lt_on_lt_table` applies, the verification that the link_type_id is exactly one character long. Therefore only one new trigger is required. @@ -116,7 +107,7 @@ exactly one character long. Therefore only one new trigger is required. .. _deleting_ltype: Removing a link_type from the link_types table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +'''''''''''''''''''''''''''''''''''''''''''''' In counterpoint, only the second behaviour mentioned above on :ref:`editing_lt_on_lt_table` applies in this case, the verification that the old diff --git a/docs/source/modeling_with_aequilibrae/project_database/matrices.rst b/docs/source/modeling_with_aequilibrae/project_database/matrices.rst new file mode 100644 index 000000000..c87eb7067 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project_database/matrices.rst @@ -0,0 +1,18 @@ +.. _matrix_table: + +Matrices +~~~~~~~~ + +The **matrices** table in the project_database is nothing more than an index of +all matrix files contained in the matrices folder inside the AequilibraE project. +This index, which looks like below, has two main columns. The first one is the +**file_name**, which contains the actual file name in disk as to allow +AequilibraE to find the file, and **name**, which is the name by which the user +should refer to the matrix in order to access it through the API. + +.. image:: ../../images/matrices_table.png + :width: 1000 + :alt: Matrices table structure + +As AequilibraE is fully compatible with OMX, the index can have a mix of matrix +types (AEM and OMX) without prejudice to functionality. \ No newline at end of file diff --git a/docs/source/project_docs/modes.rst b/docs/source/modeling_with_aequilibrae/project_database/modes.rst similarity index 76% rename from docs/source/project_docs/modes.rst rename to docs/source/modeling_with_aequilibrae/project_database/modes.rst index 9c115d67d..a884a3f8c 100644 --- a/docs/source/project_docs/modes.rst +++ b/docs/source/modeling_with_aequilibrae/project_database/modes.rst @@ -1,36 +1,35 @@ .. _tables_modes: - Modes table -=========== +~~~~~~~~~~~ The **modes** table exists to list all the modes available in the model's network, and its main role is to support the creation of graphs directly from the SQLite project. -The modes table has five fields, being the *mode_name*, *mode_id*, *description* -*pce*, *vot*, and *ppv* (persons per vehicle), where *mode_id* is a single letter -that is used to codify mode permissions in the network, as further discussed in -:ref:`network`. +.. note:: + + **Modes must have a unique mode_id composed of a single letter, which is** + **case-sensitive to a total of 52 possible modes in the model.** + +As described in the SQL data model, all AequilibraE models are created with 4 +standard modes, which can be added to or removed by the user, and would look like +the following. -An example of what the contents of the mode table look like is below: +.. image:: ../../images/modes_table.png + :width: 500 + :alt: Modes table structure -.. image:: ../images/modes_table.png - :width: 750 - :align: center - :alt: Link examples Consistency triggers --------------------- +^^^^^^^^^^^^^^^^^^^^ As it happens with the links and nodes table (:ref:`network_triggers_behaviour`), the modes table is kept consistent with the links table through the use of database triggers. .. _changing_modes_for_link: - Changing the modes allowed in a certain link -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - +'''''''''''''''''''''''''''''''''''''''''''' Whenever we change the modes allowed on a link, we need to check for two conditions: @@ -41,29 +40,26 @@ For each condition, a specific trigger was built, and if any of the checks fails, the transaction will fail. Having successfully changed the modes allowed in a link, we need to -update the nodes that are accessible to each of the nodes which are the +update the modes that are accessible to each of the nodes which are the extremities of this link. For this purpose, a further trigger is created to update the modes field in the nodes table for both of the link's a_node and b_node. Directly changing the modes field in the nodes table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - +'''''''''''''''''''''''''''''''''''''''''''''''''''' A trigger guarantees that the value being inserted in the field is according to the values found in the associated links' modes field. If the user attempts to overwrite this value, it will automatically be set back to the appropriate value. .. _adding_new_link: - Adding a new link -^^^^^^^^^^^^^^^^^ +''''''''''''''''' The exact same behaviour as for :ref:`changing_modes_for_link` applies in this case, but it requires specific new triggers on the **creation** of the link. .. _editing_mode: - Editing a mode in the modes table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +''''''''''''''''''''''''''''''''' Whenever we want to edit a mode in the modes table, we need to check for two conditions: @@ -76,20 +72,16 @@ fails, the transaction will fail. The requirements for uniqueness and non-absent values are guaranteed during the construction of the modes table by using the keys **UNIQUE** and **NOT NULL**. - .. _adding_new_mode: - Adding a new mode to the modes table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +'''''''''''''''''''''''''''''''''''' In this case, only the first behaviour mentioned above on :ref:`editing_mode` applies, the verification that the mode_id is exactly one character long. Therefore only one new trigger is required. .. _deleting_a_mode: - Removing a mode from the modes table -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - +'''''''''''''''''''''''''''''''''''' In counterpoint, only the second behaviour mentioned above on :ref:`editing_mode` applies in this case, the verification that the old mode_id is not still in use by the network. Therefore only one new trigger is diff --git a/docs/source/modeling_with_aequilibrae/project_database/network.rst b/docs/source/modeling_with_aequilibrae/project_database/network.rst new file mode 100644 index 000000000..a3b8034ff --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project_database/network.rst @@ -0,0 +1,354 @@ +.. _network: + +Network +~~~~~~~ + +The objectives of developing a network format for AequilibraE are to provide the +users a seamless integration between network data and transportation modeling +algorithms and to allow users to easily edit such networks in any GIS platform +they'd like, while ensuring consistency between network components, namely links +and nodes. As the network is composed by two tables, **links** and **nodes**, +maintaining this consistency is not a trivial task. + +As mentioned in other sections of this documentation, the links and a nodes +layers are kept consistent with each other through the use of database triggers, +and the network can therefore be edited in any GIS platform or +programmatically in any fashion, as these triggers will ensure that +the two layers are kept compatible with each other by either making +other changes to the layers or preventing the changes. + +**We cannot stress enough how impactful this set of spatial triggers was to** +**the transportation modeling practice, as this is the first time a** +transportation network can be edited without specialized software that +**requires the editing to be done inside such software.** + +.. note:: + AequilibraE does not currently support turn penalties and/or bans. Their + implementation requires a complete overahaul of the path-building code, so + that is still a long-term goal, barred specific development efforts. + +Importing and exporting the network +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Currently AequilibraE can import links and nodes from a network from OpenStreetMaps, +GMNS, and from link layers. AequilibraE can also export the existing network +into GMNS format. There is some valuable information on these topics in the following +pages: + +* :ref:`Importing files in GMNS format ` +* :ref:`Importing from OpenStreetMaps ` +* :ref:`Importing from link layers ` +* :ref:`Exporting AequilibraE model to GMNS format ` + +Dealing with Geometries +^^^^^^^^^^^^^^^^^^^^^^^ +Geometry is a key feature when dealing with transportation infrastructure and +actual travel. For this reason, all datasets in AequilibraE that correspond to +elements with physical GIS representation, links and nodes in particular, are +geo-enabled. + +This also means that the AequilibraE API needs to provide an interface to +manipulate each element's geometry in a convenient way. This is done using the +standard `Shapely `_, and we urge you to study +its comprehensive API before attempting to edit a feature's geometry in memory. + +As we mentioned in other sections of the documentation, the user is also welcome +to use its powerful tools to manipulate your model's geometries, although that +is not recommended, as the "training wheels are off". + +Data consistency +^^^^^^^^^^^^^^^^ + +Data consistency is not achieved as a monolithic piece, but rather through the +*treatment* of specific changes to each aspect of all the objects being +considered (i.e. nodes and links) and the expected consequence to other +tables/elements. To this effect, AequilibraE has triggers covering a +comprehensive set of possible operations for links and nodes, covering both +spatial and tabular aspects of the data. + +Although the behaviour of these trigger is expected to be mostly intuitive +to anybody used to editing transportation networks within commercial modeling +platforms, we have detailed the behaviour for all different network changes in +:ref:`net_section.1` . + +This implementation choice is not, however, free of caveats. Due to +technological limitations of SQLite, some of the desired behaviors identified in +:ref:`net_section.1` cannot be implemented, but such caveats do not impact the +usefulness of this implementation or its robustness in face of minimally careful +use of the tool. + + +.. note:: + This documentation, as well as the SQL code it referes to, comes from the + seminal work done in `TranspoNet `_ + by `Pedro `_ and + `Andrew `_. + +.. _network_triggers_behaviour: + +Network consistency behaviour +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In order for the implementation of this standard to be successful, it is +necessary to map all the possible user-driven changes to the underlying data and +the behavior the SQLite database needs to demonstrate in order to maintain +consistency of the data. The detailed expected behavior is detailed below. +As each item in the network is edited, a series of checks and changes to other +components are necessary in order to keep the network as a whole consistent. In +this section we list all the possible physical (geometrical) changes to each +element of the network and what behavior (consequences) we expect from each one +of these changes. +Our implementation, in the form of a SQLite database, will be referred to as +network from this point on. + +Ensuring data consistency as each portion of the data is edited is a two part +problem: + +1. Knowing what to do when a certain edit is attempted by the user +2. Automatically applying the tests and consistency checks (and changes) + required on one + +.. _net_section.1: + +Change behavior +^^^^^^^^^^^^^^^ + +In this section we present the mapping of all meaningful operations that a user +can do to links and nodes, and you can use the table below to navigate between +each of the changes to see how they are treated through triggers. + +.. table:: + :align: center + ++--------------------------------------+-----------------------------------+ +| Nodes | Links | ++======================================+===================================+ +| :ref:`net_creating_nodes` | :ref:`net_deleting_link` | ++--------------------------------------+-----------------------------------+ +| :ref:`net_deleting_nodes` | :ref:`net_moving_link_extremity` | ++--------------------------------------+-----------------------------------+ +| :ref:`net_moving_node` | :ref:`net_reshaping_link` | ++--------------------------------------+-----------------------------------+ +| :ref:`net_add_node_field` | :ref:`net_deleting_reqfield_link` | ++--------------------------------------+-----------------------------------+ +| :ref:`net_deleting_node_field` | | ++--------------------------------------+-----------------------------------+ +| :ref:`net_modifying_node_data_entry` | | ++--------------------------------------+-----------------------------------+ + +.. _net_section.1.1: + +Node layer changes and expected behavior +'''''''''''''''''''''''''''''''''''''''' + +There are 6 possible changes envisioned for the network nodes layer, being 3 of +geographic nature and 3 of data-only nature. The possible variations for each +change are also discussed, and all the points where alternative behavior is +conceivable are also explored. + +.. _net_creating_nodes: + +Creating a node +``````````````` + +There are only three situations when a node is to be created: + +- Placement of a link extremity (new or moved) at a position where no node + already exists + +- Splitting a link in the middle + +- Creation of a centroid for later connection to the network + +In all cases a unique node ID needs to be generated for the new node, and all +other node fields should be empty. + +An alternative behavior would be to allow the user to create nodes with no +attached links. Although this would not result in inconsistent networks for +traffic and transit assignments, this behavior would not be considered valid. +All other edits that result in the creation of unconnected nodes or that result +in such case should result in an error that prevents such operation + +Behavior regarding the fields regarding modes and link types is discussed in +their respective table descriptions + +.. _net_deleting_nodes: + +Deleting a node +``````````````` + +Deleting a node is only allowed in two situations: + +- No link is connected to such node (in this case, the deletion of the node + should be handled automatically when no link is left connected to such node) + +- When only two links are connected to such node. In this case, those two links + will be merged, and a standard operation for computing the value of each field + will be applied. + +For simplicity, the operations are: Weighted average for all numeric fields, +copying the fields from the longest link for all non-numeric fields. Length is +to be recomputed in the native distance measure of distance for the projection +being used. + +A node can only be eliminated as a consequence of all links that terminated/ +originated at it being eliminated. If the user tries to delete a node, the +network should return an error and not perform such operation. + +Behavior regarding the fields regarding modes and link types is discussed in +their respective table descriptions + +.. _net_moving_node: + +Moving a node +````````````` + +There are two possibilities for moving a node: Moving to an empty space, and +moving on top of another node. + +- **If a node is moved to an empty space** + +All links originated/ending at that node will have its shape altered to conform +to that new node position and keep the network connected. The alteration of the +link happens only by changing the Latitude and Longitude of the link extremity +associated with that node. + +- **If a node is moved on top of another node** + +All the links that connected to the node on the bottom have their extremities +switched to the node on top +The node on the bottom gets eliminated as a consequence of the behavior listed +on :ref:`net_deleting_nodes` + +Behavior regarding the fields regarding modes and link types is discussed in +their respective table descriptions + +.. _net_add_node_field: + +Adding a data field +``````````````````` + +No consistency check is needed other than ensuring that no repeated data field +names exist + +.. _net_deleting_node_field: + +Deleting a data field +````````````````````` + +If the data field whose attempted deletion is mandatory, the network should +return an error and not perform such operation. Otherwise the operation can be +performed. + +.. _net_modifying_node_data_entry: + +Modifying a data entry +`````````````````````` + +If the field being edited is the node_id field, then all the related tables need +to be edited as well (e.g. a_b and b_node in the link layer, the node_id tagged +to turn restrictions and to transit stops) + +.. _net_section.1.2: + +Link layer changes and expected behavior +'''''''''''''''''''''''''''''''''''''''' + +Network links layer also has some possible changes of geographic and data-only nature. + +.. _net_deleting_link: + +Deleting a link +````````````````` + +In case a link is deleted, it is necessary to check for orphan nodes, and deal +with them as prescribed in :ref:`net_deleting_nodes`. In case one of the link +extremities is a centroid (i.e. field *is_centroid*=1), then the node should not +be deleted even if orphaned. + +Behavior regarding the fields regarding modes and link types is discussed in +their respective table descriptions. + +.. _net_moving_link_extremity: + +Moving a link extremity +``````````````````````` + +This change can happen in two different forms: + +- **The link extremity is moved to an empty space** + +In this case, a new node needs to be created, according to the behavior +described in :ref:`net_creating_nodes` . The information of node ID (A or B +node, depending on the extremity) needs to be updated according to the ID for +the new node created. + +- **The link extremity is moved from one node to another** + +The information of node ID (A or B node, depending on the extremity) needs to be +updated according to the ID for the node the link now terminates in. + +Behavior regarding the fields regarding modes and link types is discussed in +their respective table descriptions. + +.. _net_reshaping_link: + +Re-shaping a link +````````````````` + +When reshaping a link, the only thing other than we expect to be updated in the +link database is their length (or distance, in AequilibraE's field structure). +As of now, distance in AequilibraE is **ALWAYS** measured in meters. + +.. _net_deleting_reqfield_link: + +Deleting a required field +````````````````````````` +Unfortunately, SQLite does not have the resources to prevent a user to remove a +data field from the table. For this reason, if the user removes a required +field, they will most likely corrupt the project. + + +.. _net_section.1.3: + +Field-specific data consistency +''''''''''''''''''''''''''''''' +Some data fields are specially sensitive to user changes. + +.. _net_change_link_distance: + +Link distance +````````````` + +Link distance cannot be changed by the user, as it is automatically recalculated +using the Spatialite function *GeodesicLength*, which always returns distances +in meters. + +.. _net_change_link_direc: + +Link direction +`````````````` + +Triggers enforce link direction to be -1, 0 or 1, and any other value results in +an SQL exception. + +.. _net_change_link_modes: + +*modes* field (Links and Nodes layers) +`````````````````````````````````````` +A serious of triggers are associated with the modes field, and they are all +described in the :ref:`tables_modes`. + +.. _net_change_link_ltypes: +*link_type* field (Links layer) & *link_types* field (Nodes layer) +`````````````````````````````````````````````````````````````````` +A serious of triggers are associated with the modes field, and they are all +described in the :ref:`tables_link_types`. + +.. _net_change_link_node_ids: +a_node and b_node +````````````````` +The user should not change the a_node and b_node fields, as they are controlled +by the triggers that govern the consistency between links and nodes. It is not +possible to enforce that users do not change these two fields, as it is not +possible to choose the trigger application sequence in SQLite diff --git a/docs/source/project_docs/parameters_metadata.rst b/docs/source/modeling_with_aequilibrae/project_database/parameters_metadata.rst similarity index 87% rename from docs/source/project_docs/parameters_metadata.rst rename to docs/source/modeling_with_aequilibrae/project_database/parameters_metadata.rst index f4e7651b9..60060cdb6 100644 --- a/docs/source/project_docs/parameters_metadata.rst +++ b/docs/source/modeling_with_aequilibrae/project_database/parameters_metadata.rst @@ -1,8 +1,7 @@ .. _parameters_metadata: -========================= Parameters metadata table -========================= +~~~~~~~~~~~~~~~~~~~~~~~~~ Documentation is paramount for any successful modeling project. For this reason, AequilibraE has a database table dedicated to the documentation of each field in diff --git a/docs/source/modeling_with_aequilibrae/project_database/results.rst b/docs/source/modeling_with_aequilibrae/project_database/results.rst new file mode 100644 index 000000000..cb39c29ac --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project_database/results.rst @@ -0,0 +1,19 @@ +.. _tables_results: + +Results +~~~~~~~ + +The **results** table exists to hold the metadata for the results stored in the +**results_database.sqlite** in the same folder as the model database. In that, +the *table_name* field is unique and must match exactly the table name in the +**results_database.sqlite**. + +Although those results could as be stored in the model database, it is possible +that the number of tables in the model file would grow too quickly and would +essentially clutter the **project_database.sqlite**. + +As a simple table, it looks as follows: + +.. image:: ../../images/results_table.png + :width: 800 + :alt: results table structure diff --git a/docs/source/modeling_with_aequilibrae/project_database/zones.rst b/docs/source/modeling_with_aequilibrae/project_database/zones.rst new file mode 100644 index 000000000..8b21c327b --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/project_database/zones.rst @@ -0,0 +1,14 @@ +.. _tables_zones: + +Zones table +~~~~~~~~~~~ + +The default **zones** table has a **MultiPolygon** geometry type and a limited +number of fields, as most of the data is expected to be in the +**demand_database.sqlite**. + +The API for manipulation of the zones table and each one of its records is +consistent with what exists to manipulate the other fields in the database. + +As it happens with links and nodes, zones also have geometries associated with +them, and in this case they are of the type . diff --git a/docs/source/modeling_with_aequilibrae/public_transport.rst b/docs/source/modeling_with_aequilibrae/public_transport.rst new file mode 100644 index 000000000..92bb3e429 --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/public_transport.rst @@ -0,0 +1,24 @@ +.. _public_transport_database: + +Public Transport database +========================= + +AequilibraE is capable of importing a General Transit Feed Specification (GTFS) feed +into its database. The Transit module has been updated in version 0.9.0. More details on +the **public_transport.sqlite** are discussed on a nearly *per-table* basis below, and +we recommend understanding the role of each table before setting an AequilibraE model +you intend to use. If you don't know much about GTFS, we strongly encourage you to take +a look at the documentation provived by `Google `_. + +.. toctree:: + :maxdepth: 1 + + transit_database/ + +A more technical view of the database structure, including the SQL queries used to create +each table and their indices are also available. + +.. toctree:: + :maxdepth: 1 + + transit_database/data_model/datamodel.rst \ No newline at end of file diff --git a/docs/source/overview.rst b/docs/source/modeling_with_aequilibrae/software_components.rst similarity index 64% rename from docs/source/overview.rst rename to docs/source/modeling_with_aequilibrae/software_components.rst index 5ad429849..34b791549 100644 --- a/docs/source/overview.rst +++ b/docs/source/modeling_with_aequilibrae/software_components.rst @@ -1,23 +1,11 @@ -An overview of AequilibraE -========================== - -.. toctree:: - :maxdepth: 4 - -AequilibraE is the first comprehensive Python package for transportation -modeling, and it aims to provide all the resources not easily available from -other open-source packages in the Python (NumPy, really) ecosystem. - -AequilibraE has also a fully features interface avalaible as a plugin for the -open source software QGIS, which is separately mantained and discussed in -detail its `documentation. `_ +.. _software_components: Sub-modules -~~~~~~~~~~~ +----------- -AequilibraE is organized in submodules that are often derived from the -traditional 4-step model. However, other modules have already been added or are -expected to be added in the future. The current modules are: +AequilibraE is organized in submodules organized around common workflows +used in transport modeling, as well as connected to the maintenance and +operation of models. The current modules are: - :ref:`overview_project` - :ref:`overview_parameters` @@ -26,28 +14,6 @@ expected to be added in the future. The current modules are: - :ref:`overview_transit` - :ref:`overview_matrix` -Contributions can be made to the existing modules or in the form of new modules. - - -.. _overview_project: - -AequilibraE Project -~~~~~~~~~~~~~~~~~~~ -The AequilibraE Project, as a consistent model file, comes from the vision of -having a complete model system that would have capabilities to support the vast -majority of analysis usually performed with traditional transport models. - -In a nutshell, the AequilibraE project file is designed to behave like many of -the commercial platforms available in the market, where a single file hosts the -majority of the data. Differently then these platforms, however, AequilibraE is -designed on top of an open data format, SQLite. - -This concept is still under heavy development, but it is already possible to use -its structure to download full, routable networks directly from -`Open Street Maps `_ . - -More detailed is provided in - :ref:`project`. - .. _overview_parameters: @@ -62,7 +28,7 @@ There are currently 4 main sessions with the parameters file: *Assignment*, The parameters for *assignment* and *distribution* control only convergence criteria, while the *System* section controls things like the number of CPU -cores used by the software, default directories and Spatialite location for +cores used by the software, default directories, and Spatialite location for Windows systems. The *Network* section, however, contains parameters that control the creation of networks and the import from Open Street Maps. @@ -89,7 +55,7 @@ Path computation ~~~~~~~~~~~~~~~~ The path computation module contains some of the oldest code in AequilibraE, -some of which preceed the existance of AequilibraE as a proper Python package. +some of which preceeded the existence of AequilibraE as a proper Python package. The package is built around a shortest path algorithm ported from SciPy and adapted to support proper multi-threading, network loading and multi-field @@ -107,7 +73,7 @@ concurrent shortest paths computation, although the path computation path does release the `GIL `_, which allows the users to get some performance gains using Python's threading module. -A wealth of usage examples are available in the examples page under +A wealth of usage examples are available on the examples page under :ref:`example_usage_paths`. @@ -116,8 +82,8 @@ A wealth of usage examples are available in the examples page under Transit ~~~~~~~ -For now the only transit-related capability of AequilibraE is to import GTFS -into SQLite/Spatialite. The results of this import is NOT integrated with the +For now, the only transit-related capability of AequilibraE is to import GTFS +into SQLite/Spatialite. The results of this import are NOT integrated with the AequilibraE project. .. Usage examples can be found on :ref:`example_usage_transit`. @@ -128,16 +94,16 @@ AequilibraE project. Matrix ~~~~~~ -The matrix submodule has two main components. Datasets and Matrices +The matrix submodule has two main components: *Datasets* and *Matrices*. Their existence is required for performance purposes and to support consistency -across other modules. It also make it a lot faster to develop new features. -Compatibility with de-facto open standards is also pursued as a major +across other modules. It also makes it a lot faster to develop new features. +Compatibility with de facto open standards is also pursued as a major requirement. -They are both memory mapped structures, which allows for some nice features, +They are both memory-mapped structures, which allows for some nice features, but they still consume all memory necessary to handle them in full. In the -future we will look into dropping that requirement, but the software work is +future, we will look into dropping that requirement, but the software work is substantial. AequilibraE Matrix @@ -175,3 +141,4 @@ AequilibraE data currently supports export to **csv** and **sqlite**. Extending it to other binary files such as HDF5 or `Arrow `_ are being considered for future development. If you require them, please file an issue on GitHub. + diff --git a/docs/source/modeling_with_aequilibrae/transit_database/datamodel.rst.template b/docs/source/modeling_with_aequilibrae/transit_database/datamodel.rst.template new file mode 100644 index 000000000..d732d4b0a --- /dev/null +++ b/docs/source/modeling_with_aequilibrae/transit_database/datamodel.rst.template @@ -0,0 +1,30 @@ +.. _transit_supply_data_model: + +SQL Data model +^^^^^^^^^^^^^^ + +The data model presented in this section pertains only to the structure of +AequilibraE's public_transport database and general information about the usefulness +of specific fields, especially on the interdependency between tables. + +Conventions +''''''''''' + +A few conventions have been adopted in the definition of the data model and some +are listed below: + +- Geometry field is always called **geometry** +- Projection is 4326 (WGS84) +- Tables are all in all lower case + + +.. Do not touch below this line unless you know EXACTLY what you are doing. +.. it will be automatically populated + +Project tables +'''''''''''''' + +.. toctree:: + :maxdepth: 1 + + LIST_OF_TABLES \ No newline at end of file diff --git a/docs/source/paragrah_marker_hierarchy.txt b/docs/source/paragrah_marker_hierarchy.txt new file mode 100644 index 000000000..4b1ac9440 --- /dev/null +++ b/docs/source/paragrah_marker_hierarchy.txt @@ -0,0 +1,9 @@ +We use the following hierarchy of header markers through the documentation +H1: = +H2: - +H3: ~ +H4: ^ +H5: ' +H6: ` +H7: \ +H8: | diff --git a/docs/source/path_computation_engine.rst b/docs/source/path_computation_engine.rst deleted file mode 100644 index 115a0f11b..000000000 --- a/docs/source/path_computation_engine.rst +++ /dev/null @@ -1,165 +0,0 @@ -.. _aequilibrae_as_path_engine: - -Path computation engine -======================= - -Given AequilibraE's incredibly fast path computation capabilities, one of its -important use cases is the computation of paths on general transportation -networks and between any two nodes, regardless of their type (centroid or not). - -This use case supports the development of a number of computationally intensive -systems, such as map matching of GPS data, simulation of Demand Responsive -Transport (DRT, e.g. Uber) operators. - -This capability is implemented within an specific class *PathResults*, which is -fully documented in the :ref:`aequilibrae_api` section of this documentation. - -Below we detail its capability for a number of use-cases outside traditional -modeling, from a simple path computation to a more sophisticated map-matching -use. -` -Basic setup - -:: - - from aequilibrae import Project - from aequilibrae.paths.results import PathResults - - proj_path = 'D:/release/countries/United Kingdom' - - proj = Project() - proj.open(proj_path) - - # We assume we are going to compute walking paths (mode *w* in our example) - # We also assume that we have fields for distance and travel time in the network - proj.network.build_graphs(['distance', 'travel_time'], modes = 'w') - - # We get the graph - graph = proj.network.graphs['w'] - - # And prepare it for computation - - # Being primarily a modeling package, AequilibraE expects that your network - # will have centroids (synthetic nodes) and connectors (synthetic links) - # and we therefore need to account for it when computing paths - # Here we will assume that we do not have centroids in the network, so - # we will have to *trick* the Graph object - - # let's get 10 of our nodes (completely arbitrary, do as you please) to - # serve as *centroids* - # The AequilibraE project file is based on SQLite, so we can just do a query - curr = proj.conn.cursor() - curr.execute('Select node_id from Nodes WHERE modes like "%c%" limit 100') - nodes = list(set([x[0] for x in curr.fetchall()])) - - # Just use the arbitrary node set as centroids - graph.prepare_graph(np.array(nodes)) - - # Tell AequilibraE that no link is synthetic (no need to block paths going through *"centroids"*). - graph.set_blocked_centroid_flows(False) - - # We will minimize travel_time - graph.set_graph('travel_time') - - # And *skim* (compute the corresponding) distance for the resulting paths - # you should do this ONLY if you require skims for any field other than the minimizing field - # or for all the nodes in the graph - # It can increase computation time in up to 30% - graph.set_skimming(['distance', 'travel_time']) - - # Finally, we get the path result computation object and prepare it to work with our graph - res = PathResults() - res.prepare(g) - - # We are now ready to compute paths between any two nodes in the network - - -path computation and finding your way around --------------------------------------------- - -Building on the code above, we can just compute paths between two arbitrary -nodes. - -:: - - res.compute_path(32568, 179) - - # You can consult the origin & destination for the path you computed - res.origin - res.destination - - # You can also consult the sequence of links traversed from origin to destination - res.path - - # And the sequence of nodes visited in that path - res.path_nodes - - # You can also know the direction you traversed each link with - res.path_link_directions # Array of the same size as res.path - - # If you chose to compute skims, you can access them for ALL NODES - # Array is indexed on node IDs - res.skims - # Order of columns is the same as in - graph.skim_fields - # disconnected and non-existing nodes are set to np.inf - - # The metric used to compute the path is also summarized for all nodes along the path - res.milepost - # This is especially useful when you want to interpolate other metrics along the path - # This is the case in route-reconstruction when map-matching GPS data - - # The shortest path tree is stored during computation, so recomputing the path from - # the same origin but for a different destination is lightning fast - res.update_trace(195) - - # Skims obviously won't change, but the OD pair specific data will - res.path_nodes - res.path - res.path_link_directions - res.milepost - - -Network skimming ----------------- -If your objective is just to compute distance/travel_time/your_own_cost matrix -between a series of nodes, then the process is even simpler - - -:: - - from aequilibrae.paths.results import SkimResults - - res.compute_path(32568, 179) - - # You can consult the origin & destination for the path you computed - res.origin - res.destination - - # You would prepare the graph with "centroids" that correspond to the nodes - # you are interested in - graph.prepare_graph(np.array(my_nodes_of_interest)) - - # And do the steps from the setup phase accordingly - graph.set_blocked_centroid_flows(False) - graph.set_graph('travel_time') - graph.set_skimming(['distance', 'travel_time']) - - # Finally, we get the path result computation object and prepare it to work with our graph - skm_res = SkimResults() - skm_res.prepare(graph) - - # You can tell AequilibraE to use an specific number of cores - skm_res.set_cores(12) - - # And then compute it - skm_res.compute_skims() - - skm_res.skims.export('path/to/matrix.omx') - # or - skm_res.skims.export('path/to/matrix.aem') - # or - skm_res.skims.export('path/to/matrix.csv') - - - diff --git a/docs/source/project.rst b/docs/source/project.rst deleted file mode 100644 index 38d582c81..000000000 --- a/docs/source/project.rst +++ /dev/null @@ -1,125 +0,0 @@ -.. _project: - -The AequilibraE project -======================= - -Similarly to commercial packages, AequilibraE is moving towards having the -majority of a model's components residing inside a single file. This change is motivated -by the fact that it is easier to develop and maintain documentation for models -if they are kept in a format that favors data integrity and that supports a -variety of data types and uses. - -.. note:: - As of now, only projection WGS84, 4326 is supported in AequilibraE. - Generalization is not guaranteed, but should come with time. - -The chosen format for AequilibraE is `SQLite `_, -with all the GIS capability supported by -`SpatiaLite `_. The -impressive performance, portability, self containment and open-source character -of these two pieces of software, along with their large user base and wide -industry support make them solid options to be AequilibraE's data backend. -Since working with Spatialite is not just a matter of *pip installing* a -package, please refer to :ref:`dependencies`. - -.. note:: - AequilibraE 0.7.0 brought and important changes to the project structure and - the API. Versions 0.8 and beyond should see a much more stable API, with new - capabilities being incorporated after that. - -Project structure ------------------ -Since version 0.7, the AequilibraE project consists of a main folder, where a -series of files and sub folders exist. The files are the following: - -- **project_database.sqlite** - Main project file, containing network and data - tables for the project - -- **results_database.sqlite** - Database containing outputs for all algorithms - such as those resulting from traffic assignment - -- **parameters.yml** - Contains parameters for all parameterized AequilibraE - procedures - -- **matrices** (*folder*) - Contains all matrices to be used within a project - -- **scenarios** (*folder*) - Contains copies of each *project_database.sqlite* - at the time a certain scenario was saved (upcoming in version 0.8) - -Data consistency ----------------- - -One of the key characteristics of any modelling platform is the ability of the -supporting software to maintain internal data consistency. Network data -consistency is surely the most critical and complex aspect of overall data -consistency, which has been introduced in the AequilibraE framework with -`TranspoNET `_, where -`Andrew O'Brien `_ -implemented link-node consistency infrastructure in the form of spatialite -triggers. - -Further data consistency, especially for tabular data, is also necessary. This -need has been largely addressed in version 0.7, but more triggers will most -likely be added in upcoming versions. - -All consistency triggers/procedures are discussed in parallel with the -features they implement. - - -Dealing with Geometries ------------------------ -Geometry is a key feature when dealing with transportation infrastructure and -actual travel. For this reason, all datasets in AequilibraE that correspond to -elements with physical GIS representation are geo-enabled. - -This also means that the AequilibraE API needs to provide an interface to -manipulate each element's geometry in a convenient way. This is done using the -wonderful `Shapely `_, and we urge you to study -its comprehensive API before attempting to edit a feature's geometry in memory. - -As AequilibraE is based on Spatialite, the user is also welcome to use its -powerful tools to manipulate your model's geometries, although that is not -recommended, as the "training wheels are off". - - -Project database ----------------- -.. toctree:: - :maxdepth: 1 - - project_docs/about - project_docs/network - project_docs/modes - project_docs/link_types - project_docs/matrices - project_docs/zones - project_docs/parameters_metadata - project_docs/results - -Parameters file ----------------- -.. toctree:: - :maxdepth: 1 - - parameter_file - -Extra user data fields -~~~~~~~~~~~~~~~~~~~~~~ -The AequilibraE standard configuration is not particularly minimalist, but it is -reasonable to expect that users would require further data fields in one or more -of the data tables that are part of the AequilibraE project. For this reason, and -to incentivate the creation of a consistent culture around the handling of model -data in aequilibrae, we have added 10 additional data fields to each table which -are not used by AequilibraE's standard configuration, all of which are named as -Greek letters. They are the following: - -- 'alpha' -- 'beta' -- 'gamma' -- 'delta' -- 'epsilon' -- 'zeta' -- 'iota' -- 'sigma' -- 'phi' -- 'tau' \ No newline at end of file diff --git a/docs/source/project_docs/about.rst b/docs/source/project_docs/about.rst deleted file mode 100644 index e938ddf6c..000000000 --- a/docs/source/project_docs/about.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. _tables_about: - -=========== -About table -=========== - -The **about** table is the simplest of all tables in the AequilibraE project, -but it is the one table that contains the documentation about the project, and -it is therefore crucial for project management and quality assurance during -modelling projects. - -New AequilibraE projects will be created with 12 default metadata fields, listed -in the table below (with examples). - -+--------------------------+-----------------------------------------------------------------------+ -| **Metadata** | **Description** | -+==========================+=======================================================================+ -| **model_name** | Name of the model. (e.g. Alice Spring Freight Forecasting model) | -+--------------------------+-----------------------------------------------------------------------+ -| **region** | Alice Springs, Northern Territory - Australia | -+--------------------------+-----------------------------------------------------------------------+ -| **description** | Freight model for the 200km circle centered in Alice Springs | -+--------------------------+-----------------------------------------------------------------------+ -| **author** | John Doe | -+--------------------------+-----------------------------------------------------------------------+ -| **license** | GPL | -+--------------------------+-----------------------------------------------------------------------+ -| **scenario_name** | Optimistic scenario | -+--------------------------+-----------------------------------------------------------------------+ -| **scenario_description** | Contains hypothesis that we will find infinite petroleum here | -+--------------------------+-----------------------------------------------------------------------+ -| **year** | 2085 | -+--------------------------+-----------------------------------------------------------------------+ -| **model_version** | 2025.34.75 | -+--------------------------+-----------------------------------------------------------------------+ -| **project_id** | f3a43347aa0d4755b2a7c4e06ae1dfca | -+--------------------------+-----------------------------------------------------------------------+ -| **aequilibrae_version** | 0.7.4 | -+--------------------------+-----------------------------------------------------------------------+ -| **projection** | 4326 | -+--------------------------+-----------------------------------------------------------------------+ - -However, it is possible to create new information fields programatically. Once -the new field is added, the underlying database is altered and the field will -be present when the project is open during future use. - -:: - - p = Project() - p.open('my/project/folder') - p.about.add_info_field('my_super_relevant_field') - p.about.my_super_relevant_field = 'super relevant information' - p.about.write_back() - -Changing existing fields can also be done programatically - -:: - - p = Project() - p.open('my/project/folder') - p.about.scenario_name = 'Just a better scenario name' - p.about.write_back() - -In both cases, the new values for the information fields are only saved to disk -if *write_back()* is invoked. - -We strongly recommend not to edit the information on **projection** and -**aequilibrae_version**, as these are fields that might or might not be used by -the software to produce valuable information to the user with regards to -opportunities for version upgrades. \ No newline at end of file diff --git a/docs/source/project_docs/matrices.rst b/docs/source/project_docs/matrices.rst deleted file mode 100644 index e62f87103..000000000 --- a/docs/source/project_docs/matrices.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. _matrix_table: - -======== -Matrices -======== - -The infrastructure around matrices in AequilibraE is composed of an index table, -**matrices**, in the main project file that lists all matrices associated with -the project, and a folder inside the project main, which contains the actual -matrix files. - -The need for having matrices sit outside the model file are two-fold: I/O -performance and to keep the model file with a more manageable size. -Have you ever wondered while every single transportation modeling platform keeps -matrices in separate files? It turns out this is a quite obvious design decision -given current models and available computer hardware. - -Finally, AequilibraE is fully compatible with OMX, so you can keep (and generate) -all your matrices in that format. - diff --git a/docs/source/project_docs/network.rst b/docs/source/project_docs/network.rst deleted file mode 100644 index cce254e16..000000000 --- a/docs/source/project_docs/network.rst +++ /dev/null @@ -1,585 +0,0 @@ -.. _network: - -======= -Network -======= - -.. note:: - This documentation, as well as the SQL code it referred to, comes from the - seminal work done in `TranspoNet `_ - by `Pedro `_ and - `Andrew `_. - -The objectives of developing a network format for AequilibraE are to provide the -users a seamless integration between network data and transportation modeling -algorithms and to allow users to easily edit such networks in any GIS platform -they'd like, while ensuring consistency between network components, namely links -and nodes. - -As mentioned in other sections of this documentation, the AequilibraE -network file is composed by a links and a nodes layer that are kept -consistent with each other through the use of database triggers, and -the network can therefore be edited in any GIS platform or -programatically in any fashion, as these triggers will ensure that -the two layers are kept compatible with each other by either making -other changes to the layers or preventing the changes. - -Although the behaviour of these trigger is expected to be mostly intuitive -to anybody used to editing transportation networks within commercial modeling -platforms, we have detailed the behaviour for all different network changes in -:ref:`net_section.1` . - -This implementation choice is not, however, free of caveats. Due to -technological limitations of SQLite, some of the desired behaviors identified in -:ref:`net_section.1` cannot be implemented, but such caveats do not impact the -usefulness of this implementation or its robustness in face of minimally careful -use of the tool. - -.. note:: - AequilibraE does not currently support turn penalties and/or bans. Their - implementation requires a complete overahaul of the path-building code, so - that is still a long-term goal, barred specific developed efforts. - -Network Fields --------------- - -As described in the :ref:`project` the AequilibraE network is composed of two layers (links -and nodes), detailed below. - -Links -~~~~~ - -Network links are defined by geographic elements of type LineString (No -MultiLineString allowed) and a series of required fields, as well a series of -other optional fields that might be required for documentation and display -purposes (e.g. street names) or by specific applications (e.g. parameters for -Volume-Delay functions, hazardous vehicles restrictions, etc.). - -Below we present the - -**The mandatory fields are the following. REMOVING ANY OF THESE FIELDS WILL CORRUPT YOUR NETWORK** - -+-------------+-----------------------------------------------------------------------+-------------------------+ -| Field name | Field Description | Data Type | -+=============+=======================================================================+=========================+ -| link_id | Unique identifier | Integer (32/64 bits) | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| a_node | node_id of the first (topologically) node of the link | Integer (32/64 bits) | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| b_node | node_id of the last (topologically) node of the link | Integer (32/64 bits) | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| direction | Direction of flow allowed for the link (A-->B: 1, B-->A:-1, Both:0) | Integer 8 bits | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| distance | Length of the link in meters | Float 64 bits | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| modes | Modes allowed in this link. (Concatenation of mode ids) | String | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| link_type | Link type classification. Can be the highway tag for OSM or other | String | -+-------------+-----------------------------------------------------------------------+-------------------------+ - - -**The following fields are generated when a new AequilibraE model is created,** -**but may be removed without compromising the network's consistency.** - -+-------------+-----------------------------------------------------------------------+-------------------------+ -| Field name | Field Description | Data Type | -+=============+=======================================================================+=========================+ -| name | Cadastre name of the street | String | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| capacity_ab | Modeling capacity of the link for the direction A --> B | Float 32 bits | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| capacity_ba | Modeling capacity of the link for the direction B --> A | Float 32 bits | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| speed_ab | Modeling (Free flow) speed for the link in the A --> B direction | Float 32 Bits | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| speed_ab | Modeling (Free flow) speed for the link in the B --> A direction | Float 32 bits | -+-------------+-----------------------------------------------------------------------+-------------------------+ - -The user is free to add a virtually unlimited number of fields to the network. -Although we recommend adding fields using the Python API, adding fields -directly to the database should not create any issues, but one should observe -the convention that direction-specific information should be added in the -form of two fields with the suffixes *_ab* & *_ba*. - -Nodes -~~~~~ - -The nodes table only has four mandatory fields as of now: *node_id*, which are -directly linked to *a_node* and *b_node* in the links table through a series of -database triggers, *is_centroid*, which is a binary 1/0 value identifying nodes -as centroids (1) or not (0). - -The fields for *modes* and *link_types* are linked to the *modes* and -*link_type* fields from the links layer through a series of triggers, and -cannot be safely edited by the user (nor there is reason for such). - -+-------------+-----------------------------------------------------------------------+-------------------------+ -| Field name | Field Description | Data Type | -+=============+=======================================================================+=========================+ -| node_id | Unique identifier. Tied to the link table's a_node & b_node | Integer (32/64 bits) | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| is_centroid | node_id of the first (topologically) node of the link | Integer (32/64 bits) | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| modes | Concatenation of all mode_ids of all links connected to the node | String | -+-------------+-----------------------------------------------------------------------+-------------------------+ -| link_types | Concatenation of all link_type_ids of all links connected to the node | String | -+-------------+-----------------------------------------------------------------------+-------------------------+ - -As it is the case for the lin k layer, the user is welcome to add new fields -directly to the database, but we recommend using the API. - -.. note:: - It is good practice when working with the sqlite to keep all field names without - spaces and all lowercase. **SPACES AND NUMBERS IN THE FIELD NAMES ARE NOT** - **SUPPORTED** - -Future components -~~~~~~~~~~~~~~~~~ - -3. Turn penalties/restrictions - -4. Transit routes - -5. Transit stops - -.. _importing_from_osm: - -Importing from Open Street Maps -------------------------------- - -Please review the information :ref:`parameters_osm` - -.. note:: - - **ALL links that cannot be imported due to errors in the SQL insert** - **statements are written to the log file with error message AND the SQL** - **statement itself, and therefore errors in import can be analyzed for** - **re-downloading or fixed by re-running the failed SQL statements after** - **manual fixing** - -.. _sqlite_python_limitations: - -Python limitations -~~~~~~~~~~~~~~~~~~ -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. - -:: - - 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 - :width: 1383 - :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. - -:: - - 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 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 -platform that does not support both RTree and Spatialite. - -.. _importing_from_gmns: - -Importing from files in GMNS format ------------------------------------ - -Before importing a network from a source in GMNS format, it is imperative to know -in which spatial reference its geometries (links and nodes) were created. If the SRID -is different than 4326, it must be passed as an input using the argument 'srid'. - -You can import a GMNS network as shown below: - -:: - from aequilibrae.project import Project - - p = Project() - p.new('path/to/project/new/folder') - p.network.create_from_gmns(link_file_path='path/to/link_file.csv', node_file_path='path/to/node_file.csv', srid=32619) - # p.network.create_from_gmns( - # link_file_path='path/to/link_file.csv', node_file_path='path/to/node_file.csv', - # use_group_path='path/to/use_group.csv', geometry_path='path/to/geometry.csv', srid=32619 - # ) - p.conn.close() - -As of July 2022, it is possible to import the following files from a GMNS source: - -* link table; -* node table; -* use_group table; -* geometry table. - -You can find the specification for all these tables in the GMNS documentation, -`here `_. - -By default, the method *create_from_gmns()* read all required and optional fields -specified in the GMNS link and node tables specification. If you need it to read -any additional fields as well, you have to modify the AequilibraE parameters as -shown in the :ref:`example `. -When adding a new field to be read in the parameters.yml file, it is important to -keep the "required" key set to False, since you will always be adding a non-required -field. Required fields for a specific table are only those defined in the GMNS -specification. - -.. note:: - - **In the AequilibraE nodes table, if a node is to be identified as a centroid, its** - **'is_centroid' field has to be set to 1. However, this is not part of the GMNS** - **specification. Thus, if you want a node to be identified as a centroid during the** - **import process, in the GMNS node table you have to set the field 'node_type' equals** - **to 'centroid'.** - -.. _exporting_to_gmns: - -Exporting AequilibraE model to GMNS format ------------------------------------------- - -After loading an existing AequilibraE project, you can export it to GMNS format as -shown below: - -:: - from aequilibrae.project import Project - - p = Project() - p.load('path/to/project/new/folder') - p.network.export_to_gmns(path='path/to/output/folder') - - p.conn.close() - -As of July 2022, it is possible to export an AequilibraE network to the following -tables in GMNS format: - -* link table -* node table -* use_definition table - -This list does not include the optional use_group table, which is an optional argument -of the *create_from_gmns()* function, because mode groups are not used in the AequilibraE -modes table. - -In addition to all GMNS required fileds for each of the three exported tables, some -other fields are also added as riminder of where the features came from when looking -back at the AequilibraE project. - -.. note:: - - **When a node is identified as a centroid in the AequilibraE nodes table, this** - **information is transmitted to the GMNS node table by meaans of the field** - **'node_type', which is set to 'centroid' in this case. The 'node_type' field** - **is an optinal field listed in the GMNS node table specification.** - -You can find the GMNS specification -`here `_. - -.. _network_triggers_behaviour: - -Network consistency behaviour ------------------------------ - -In order for the implementation of this standard to be successful, it is -necessary to map all the possible user-driven changes to the underlying data and -the behavior the SQLite database needs to demonstrate in order to maintain -consistency of the data. The detailed expected behavior is detailed below. -As each item in the network is edited, a series of checks and changes to other -components are necessary in order to keep the network as a whole consistent. In -this section we list all the possible physical (geometrical) changes to each -element of the network and what behavior (consequences) we expect from each one -of these changes. -Our implementation, in the form of a SQLite database, will be referred to as -network from this point on. - -Ensuring data consistency as each portion of the data is edited is a two part -problem: - -1. Knowing what to do when a certain edit is attempted by the user -2. Automatically applying the tests and consistency checks (and changes) - required on one - -.. _net_section.1: - -Change behavior -~~~~~~~~~~~~~~~ - -In this section we present the mapping of all meaningful changes that a user can -do to each part of the transportation network, doing so for each element of the -transportation network. - -.. _net_section.1.1: - -Node layer changes and expected behavior -++++++++++++++++++++++++++++++++++++++++ - -There are 6 possible changes envisioned for the network nodes layer, being 3 of -geographic nature and 3 of data-only nature. The possible variations for each -change are also discussed, and all the points where alternative behavior is -conceivable are also explored. - -.. _net_section.1.1.1: - -Creating a node -^^^^^^^^^^^^^^^ - -There are only three situations when a node is to be created: -- Placement of a link extremity (new or moved) at a position where no node -already exists -- Spliting a link in the middle -- Creation of a centroid for later connection to the network - -In both cases a unique node ID needs to be generated for the new node, and all -other node fields should be empty -An alternative behavior would be to allow the user to create nodes with no -attached links. Although this would not result in inconsistent networks for -traffic and transit assignments, this behavior would not be considered valid. -All other edits that result in the creation of un-connected nodes or that result -in such case should result in an error that prevents such operation - -Behavior regarding the fields regarding modes and link types is discussed in -their respective table descriptions - -.. _net_section.1.1.2: - -Deleting a node -^^^^^^^^^^^^^^^ - -Deleting a node is only allowed in two situations: -- No link is connected to such node (in this case, the deletion of the node -should be handled automatically when no link is left connected to such node) -- When only two links are connected to such node. In this case, those two links -will be merged, and a standard operation for computing the value of each field -will be applied. - -For simplicity, the operations are: Weighted average for all numeric fields, -copying the fields from the longest link for all non-numeric fields. Length is -to be recomputed in the native distance measure of distance for the projection -being used. - -A node can only be eliminated as a consequence of all links that terminated/ -originated at it being eliminated. If the user tries to delete a node, the -network should return an error and not perform such operation. - -Behavior regarding the fields regarding modes and link types is discussed in -their respective table descriptions - -.. _net_section.1.1.3: - -Moving a node -^^^^^^^^^^^^^ - -There are two possibilities for moving a node: Moving to an empty space, and -moving on top of another node. - -- **If a node is moved to an empty space** - -All links originated/ending at that node will have its shape altered to conform -to that new node position and keep the network connected. The alteration of the -link happens only by changing the Latitude and Longitude of the link extremity -associated with that node. - -- **If a node is moved on top of another node** - -All the links that connected to the node on the bottom have their extremities -switched to the node on top -The node on the bottom gets eliminated as a consequence of the behavior listed -on :ref:`net_section.1.1.2` - -Behavior regarding the fields regarding modes and link types is discussed in -their respective table descriptions - -.. _net_section.1.1.4: - -Adding a data field -^^^^^^^^^^^^^^^^^^^ - -No consistency check is needed other than ensuring that no repeated data field -names exist - -.. _net_section.1.1.5: - -Deleting a data field -^^^^^^^^^^^^^^^^^^^^^ - -If the data field whose attempted deletion is mandatory, the network should -return an error and not perform such operation. Otherwise the operation can be -performed. - -.. _net_section.1.1.6: - -Modifying a data entry -^^^^^^^^^^^^^^^^^^^^^^ - -If the field being edited is the node_id field, then all the related tables need -to be edited as well (e.g. a_b and b_node in the link layer, the node_id tagged -to turn restrictions and to transit stops) - -.. _net_section.1.2: - -Link layer changes and expected behavior -++++++++++++++++++++++++++++++++++++++++ - -There are 8 possible changes envisioned for the network links layer, being 5 of -geographic nature and 3 of data-only nature. - -.. _net_section.1.2.1: - -Deleting a link -^^^^^^^^^^^^^^^ - -In case a link is deleted, it is necessary to check for orphan nodes, and deal -with them as prescribed in :ref:`net_section.1.1.2` - -Behavior regarding the fields regarding modes and link types is discussed in -their respective table descriptions. - - -.. _net_section.1.2.2: - -Moving a link extremity -^^^^^^^^^^^^^^^^^^^^^^^ - -This change can happen in two different forms: - -- **The link extremity is moved to an empty space** - -In this case, a new node needs to be created, according to the behavior -described in :ref:`net_section.1.1.1` . The information of node ID (A or B -node, depending on the extremity) needs to be updated according to the ID for -the new node created. - -- **The link extremity is moved from one node to another** - -The information of node ID (A or B node, depending on the extremity) needs to be -updated according to the ID for the node the link now terminates in. - -Behavior regarding the fields regarding modes and link types is discussed in -their respective table descriptions. - -.. _net_section.1.2.3: - -Re-shaping a link -^^^^^^^^^^^^^^^^^ - -When reshaping a link, the only thing other than we expect to be updated in the -link database is their length (or distance, in AequilibraE's field structure). -As of now, distance in AequilibraE is **ALWAYS** measured in meters. - -.. .. _net_section.1.2.4: - -.. Splitting a link -.. ^^^^^^^^^^^^^^^^ -.. *To come* - -.. _net_section.1.2.5: - -.. Merging two links -.. ^^^^^^^^^^^^^^^^^ -.. *To come* - -.. _net_section.1.2.6: - -Deleting a required field -^^^^^^^^^^^^^^^^^^^^^^^^^ -Unfortunately, SQLite does not have the resources to prevent a user to remove a -data field from the table. For this reason, if the user removes a required -field, they will most likely corrupt the project. - - -.. _net_section.1.3: - -Field-specific data consistency -+++++++++++++++++++++++++++++++ - Some data fields are specially - -.. _net_section.1.3.1: - -Link distance -^^^^^^^^^^^^^ - -Link distance cannot be changed by the user, as it is automatically recalculated -using the Spatialite function *GeodesicLength*, which always returns distances -in meters. - -.. _net_section.1.3.2: - -Link direction -^^^^^^^^^^^^^^ - -Triggers enforce link direction to be -1, 0 or 1, and any other value results in -an SQL exception. - - -.. _net_section.1.3.3: - -*modes* field (Links and Nodes layers) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A serious of triggers are associated with the modes field, and they are all -described in the :ref:`tables_modes`. - -*link_type* field (Links layer) & *link_types* field (Nodes layer) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -A serious of triggers are associated with the modes field, and they are all -described in the :ref:`tables_link_types`. - -a_node and b_node -^^^^^^^^^^^^^^^^^ -The user should not change the a_node and b_node fields, as they are controlled -by the triggers that govern the consistency between links and nodes. It is not -possible to enforce that users do not change these two fields, as it is not -possible to choose the trigger application sequence in SQLite - - -Projection ----------- - -Although GIS technology allows for a number of different projections to be used -in pretty much any platform, we have decided to have all AequilibraE's project -using a single projection, WGS84 - CRS 4326. - -This should not affect users too much, as GIS platforms allow for on-the-fly -reprojection for mapping purposes. - - -# 4 References -http://tfresource.org/Category:Transportation_networks - -# 5 Authors - -## Pedro Camargo -- www.xl-optim.com -- diff --git a/docs/source/project_docs/results.rst b/docs/source/project_docs/results.rst deleted file mode 100644 index b658a898c..000000000 --- a/docs/source/project_docs/results.rst +++ /dev/null @@ -1,38 +0,0 @@ -.. _tables_results: - -Results -======= - -The **results** table exists to hold the metadata for the results stored in the -*results_database.sqlite* in the same folder as the model database. - -Although those results could as be stored in the model database, it is possible -that the number of tables in the model file would grow too quickly and would -essentially clutter the *project_database.sqlite*. - -This is just a matter of software design and can change in future versions of -the software, however. - -There are four fields in this table, which should enough to precisely identify -the results, should the user take their time to input the description of the -data. - -* table_name - -The actual name of the result table in the *results_database.sqlite* - -* procedure - -The name of the procedure that generated this result (e.g. Traffic Assignment) - -* procedure_id - -Unique Alpha-numeric identifier for this procedure. This ID will be visible in -the log file and everywhere else there are references to this specific result -(e.g. in the matrix table that refers the matrices/skims generated by the same -procedure that generated this table) - -* description - -User-provided description for this result. If no information is provided, some of -AequilibraE's procedures will generate basic information for this field. diff --git a/docs/source/project_docs/zones.rst b/docs/source/project_docs/zones.rst deleted file mode 100644 index 9e8286223..000000000 --- a/docs/source/project_docs/zones.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. _tables_zones: - -Zones table -=========== - -The **zones** table exists only for the user's convenience, as it is likely to -be required in a full-blown model. As it is not required to exist, the table -created with each new model has a very limited number of fields, as follows: - -* zone_id -* area (in m\ :sup:`2`) -* name -* population -* employment -* geometry - -As it is to be expected, zone_id must be unique, but the remaining fields are -not restricted in any form. - -The API for manipulation of the zones table and each one of its records is -consistent with what exists to manipulate the other fields in the database. - -As it happens with links and nodes, zones also have geometries associated with -them, and in this case they are of the type **MultiPolygon**. - -An example of manipulating the zones table follows: - -:: - - p = Project() - p.open('my/project/folder') - zones = p.zones - - # We edit the fields for a particular zone - zone_downtown = zones.get(1) - zone_downtown.population = 637 - zone_downtown.employment = 10039 - zone_downtown.save() - - fields = zones.fields - - # We can also add one more field to the table - fields.add('parking_spots', 'Total licensed parking spots', 'INTEGER') diff --git a/docs/source/project_structure.drawio b/docs/source/project_structure.drawio new file mode 100644 index 000000000..724b2b342 --- /dev/null +++ b/docs/source/project_structure.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/docs/source/qgis.rst b/docs/source/qgis.rst deleted file mode 100644 index 58c91cf8d..000000000 --- a/docs/source/qgis.rst +++ /dev/null @@ -1,5 +0,0 @@ -QGIS -==== - -If you are looking for the documentation for AequilibraE for QGIS, you can -see it on its own webpage `aequilibrae for QGIS 3 `__ diff --git a/docs/source/traffic_assignment.rst b/docs/source/traffic_assignment.rst deleted file mode 100644 index 0b0e2ea3a..000000000 --- a/docs/source/traffic_assignment.rst +++ /dev/null @@ -1,592 +0,0 @@ -.. _traffic_assignment_description: - -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 how subjective -*complex* is). - -.. note:: - AequilibraE has had efficient multi-threaded All-or-Nothing (AoN) assignment - for a while, but since the Method-of-Successive-Averages, Frank-Wolfe, - Conjugate-Frank-Wolfe and Biconjugate-Frank-Wolfe are new in the software, it - should take some time for these implementations to reach full maturity. - -Performing traffic assignment ------------------------------ - -For a comprehensive use case for the traffic assignment module, please see the -:ref:`comprehensive_traffic_assignment_case` section of the use cases page. - - -Traffic Assignment Class -~~~~~~~~~~~~~~~~~~~~~~~~ - -Traffic assignment is organized within a object new to version 0.6.1 that -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 free-flow travel time - -* **algorithm**: The assignment algorithm to be used. e.g. "all-or-nothing" or - "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:`example_usage_parameters` . - -There are also some strict technical requirements for formulating the -multi-class equilibrium assignment as a contrained 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:`example_logging` section of the use cases. - -To begin building the assignment it is easy: - -:: - - from aequilibrae.paths import TrafficAssignment - - assig = TrafficAssignment() - -Volume Delay Function -+++++++++++++++++++++ - -For now, the only VDF functions available in AequilibraE are the BPR, - -:math:`CongestedTime_{i} = FreeFlowTime_{i} * (1 + \alpha * (\frac{Volume_{i}}{Capacity_{i}})^\beta)` - -BPR2 which double beta when traffic flow is over the link capacity, - -Spiess' conical, - -: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: - -:: - - 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. - -.. _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 composition of this class: - -* **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 (see the :ref:`multiple_user_classes` - -* **pce** - The passenger-car equivalent is the standard way of modelling - multi-class traffic assignment equilibrium in a consistent manner (see [4] 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. - -Example: - -:: - - tc = TrafficClass(graph_car, matrix_car) - - tc2 = TrafficClass(graph_truck, matrix_truck) - tc2.set_pce(1.9) - - -To add traffic classes to the assignment instance it is just a matter of making -a method call: - -:: - - assig.set_classes([tc, tc2]) - - -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**. - -To choose a field that exists in the graph, we just pass the parameters as -follows: - -:: - - assig.set_vdf_parameters({"alpha": "alphas", "beta": "betas"}) - - -To pass global values, it is simply a matter of doing the following: - -:: - - assig.set_vdf_parameters({"alpha": 0.15, "beta": 4}) - - -Setting final parameters -~~~~~~~~~~~~~~~~~~~~~~~~ - -There are still three parameters missing for the assignment. - -* Capacity field - -* Travel time field - -* Equilibrium algorithm to use - -:: - - assig.set_capacity_field("capacity") - assig.set_time_field("free_flow_time") - assig.set_algorithm(algorithm) - -Finally, one can execute assignment: - -:: - - assig.execute() - -:ref:`convergence_criteria` is discussed below. - -Multi-class Equilibrium assignment ----------------------------------- - -By introducing equilibrium assignment [1] with as many algorithms as we have, it -makes sense to also introduce multi-class assignment, adding to the pre-existing -capability of assigning multiple user-classes at once. However, multi-class -equilibrium assignments have strict technical requirements and different -equilibrium algorithms have slightly different resource requirements. - -.. note:: - Our implementations of the conjudate and Biconjugate-Frank-Wolfe methods - should be inherently proportional [6], but we have not yet carried the - appropriate testing that would be required for an empirical proof - -Cost function -~~~~~~~~~~~~~ - -It is currently not possible to use custom cost functions for assignment, and -the only cost function available to be minimized is simply travel time. - -.. _technical_requirements_multi_class: - -Technical requirements -~~~~~~~~~~~~~~~~~~~~~~ - -This documentation is not intended to discuss in detail the mathematical -requirements of multi-class traffic assignment, which can be found discussed in -detail on `Zill et all. `_ - -A few requirements, however, need to be made clear. - -* All traffic classes shall have identical free-flow travel times throughout the - network - -* Each class shall have an unique Passenger Car Equivalency (PCE) factor - -* Volume delay functions shall be monotonically increasing. *Well behaved* - functions are always something we are after - -For the conjugate and Biconjugate Frank-Wolfe algorithms it is also necessary -that the VDFs are differentiable. - -.. _convergence_criteria: - -Convergence criteria -~~~~~~~~~~~~~~~~~~~~ - -Convergence in AequilibraE is measured solely in terms of relative gap, which is -a somewhat old recommendation [5], but it is still the most used measure in -practice, and is detailed below. - -:math:`RelGap = \frac{\sum_{a}V_{a}^{*}*C_{a} - \sum_{a}V_{a}^{AoN}*C_{a}}{\sum_{a}V_{a}^{*}*C_{a}}` - -The algorithm's two stop criteria currently used are the maximum number of -iterations and the target Relative Gap, as specified above. These two parameters -are collected directly from the :ref:`parameters_file`, described in detail in -the :ref:`parameters_assignment` section. - -In order to override the parameter file values, one can set the assignment -object member variables directly before execution. - -:: - - assig.max_iter = 250 - assig.rgap_target = 0.0001 - - -Algorithms available -~~~~~~~~~~~~~~~~~~~~ - -All algorithms have been implemented as a single software class, as the -differences between them are simply the step direction and step size after each -iteration of all-or-nothing assignment, as shown in the table below - -+-------------------------------+-----------------------------------------------------------+-------------------------------------------------+ -| Algorithm | Step direction | Step Size | -+===============================+===========================================================+=================================================+ -| Method of Successive Averages | All-or-Nothing assignment (AoN) | function of the iteration number | -+-------------------------------+-----------------------------------------------------------+-------------------------------------------------+ -| Frank-Wolfe | All-or-Nothing assignment | Optimal value derived from Wardrop's principle | -+-------------------------------+-----------------------------------------------------------+-------------------------------------------------+ -| Conjugate Frank-Wolfe | Conjugate direction (Current and previous AoN) | Optimal value derived from Wardrop's principle | -+-------------------------------+-----------------------------------------------------------+-------------------------------------------------+ -| Biconjugate Frank-Wolfe | Biconjugate direction (Current and two previous AoN) | Optimal value derived from Wardrop's principle | -+-------------------------------+-----------------------------------------------------------+-------------------------------------------------+ - -Method of Successive Averages -+++++++++++++++++++++++++++++ - -This algorithm has been included largely for hystorical reasons, and we see very -little reason to use it. Yet, it has been implemented with the appropriate -computation of relative gap computation and supports all the analysis features -available. - -Frank-Wolfe (FW) -++++++++++++++++ - -The implementation of Frank-Wolfe in AequilibraE is extremely simple from an -implementation point of view, as we use a generic optimizer from SciPy as an -engine for the line search, and it is a standard implementation of the algorithm -introduced by LeBlanc in 1975 [2]. - - -Conjugate Frank-Wolfe -+++++++++++++++++++++ - -The conjugate direction algorithm was introduced in 2013 [3], which is quite -recent if you consider that the Frank-Wolfe algorithm was first applied in the -early 1970's, and it was introduced at the same as its Biconjugate evolution, -so it was born outdated. - -Biconjugate Frank-Wolfe -+++++++++++++++++++++++ - -The Biconjugate Frank-Wolfe algorithm is currently the fastest converging link- -based traffic assignment algorithm used in practice, and it is the recommended -algorithm for AequilibraE users. Due to its need for previous iteration data, -it **requires more memory** during runtime, but very large networks should still -fit nicely in systems with 16Gb of RAM. - -Implementation details & tricks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A few implementation details and tricks are worth mentioning not because it is -needed to use the software, but because they were things we grappled with during -implementation, and it would be a shame not register it for those looking to -implement their own variations of this algorithm or to slight change it for -their own purposes. - -* The relative gap is computed with the cost used to compute the All-or-Nothing - portion of the iteration, and although the literature on this is obvious, we - took some time to realize that we should re-compute the travel costs only - **AFTER** checking for convergence. - -* In some instances, Frank-Wolfe is extremely unstable during the first - iterations on assignment, resulting on numerical errors on our line search. - We found that setting the step size to the corresponding MSA value (1/ - current iteration) resulted in the problem quickly becoming stable and moving - towards a state where the line search started working properly. This technique - was generalized to the conjugate and biconjugate Frank-Wolfe algorithms. - - -Opportunities for multi-threading -+++++++++++++++++++++++++++++++++ - -Most multi-threading opportunities have already been taken advantage of during -the implementation of the All-or-Nothing portion of the assignment. However, the -optimization engine using for line search, as well as a few functions from NumPy -could still be paralellized for maximum performance on system with high number -of cores, such as the latest Threadripper CPUs. These numpy functions are the -following: - -* np.sum -* np.power -* np.fill - -A few NumPy operations have already been parallelized, and can be seen on a file -called *parallel_numpy.pyx* if you are curious to look at. - -Most of the gains of going back to Cython to paralelize these functions came -from making in-place computation using previously existing arrays, as the -instantiation of large NumPy arrays can be computationally expensive. - -References -++++++++++ - -Volume delay functions -^^^^^^^^^^^^^^^^^^^^^^ - -[1] Spiess H. (1990) "Technical Note—Conical Volume-Delay Functions." -Transportation Science, Vol 24 Issue 2. `Conical `_ - -[2] Hampton Roads Transportation Planning Organization, Regional Travel Demand Model V2 -`Technical Documentation - Final Report `_ -(2020) - -Traffic assignment and equilibrium -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -[1] Wardrop J. G. (1952) "Some theoretical aspects of road traffic research." -Proc. Inst. Civil Eng. 1 Part II, pp.325-378. - -[2] LeBlanc L. J., Morlok E. K. and Pierskalla W. P. (1975) "An efficient -approach to solving the road network equilibrium traffic assignment problem" -Transpn Res. 9, 309-318. - -[3] Maria Mitradjieva and Per Olov Lindberg (2013) "The Stiff Is Moving—Conjugate -Direction Frank-Wolfe Methods with Applications to Traffic Assignment", -`Mitradjieva and Lindberg `_ - -[4] Zill, J., Camargo, P., Veitch, T., Daisy,N. (2019) "Toll Choice and -Stochastic User Equilibrium: Ticking All the Boxes", Transportation Research -Record, Vol 2673, Issue 4 `Zill et. all `_ - -[5] Rose, G., Daskin, M., Koppelman, F. (1988) "An examination of convergence -error in equilibrium traffic assignment models", Transportation Res. B, Vol 22 -Issue 4, PP 261-274 `Rose, Daskin and Koppelman `_ - -[6] Florian, M., Morosan, C.D. (2014) "On uniqueness and proportionality in -multi-class equilibrium assignment", Transportation Research Part B, Volume 70, -pg 261-274 `Florian and Morosan `_ - -Handling the network --------------------- -The other important topic when dealing with multi-class assignment is to have -a single consistent handling of networks, as in the end there is only physical -network being handled, regardless of access differences to each mode (e.g. truck -lanes, High-Occupancy Lanes, etc.). This handling is often done with something -called a **super-network**. - -Super-network -~~~~~~~~~~~~~ -We deal with a super-network by having all classes with the same links in their -sub-graphs, but assigning b_node identical to a_node for all links whenever a -link is not available for a certain user class. -It is slightly less efficient when we are computing shortest paths, but a LOT -more efficient when we are aggregating flows. - -The use of the AequilibraE project and its built-in methods to build graphs -ensure that all graphs will be built in a consistent manner and multi-class -assignment is possible. - -Numerical Study ---------------- -Similar to other complex algorthms that handle a large amount of data through -complex computations, traffic assignment procedures can always be subject to at -least one very reasonable question: Are the results right? - -For this reason, we have used all equilibrium traffic assignment algorithms -available in AequilibraE to solve standard instances used in academia for -comparing algorithm results, some of which have are available with highly -converged solutions (~1e-14): -``_ - -Sioux Falls -~~~~~~~~~~~~ - -Network has: - -* Links: 76 -* Nodes: 24 -* Zones: 24 - -.. image:: images/sioux_falls_msa-500_iter.png - :width: 590 - :alt: Sioux Falls MSA 500 iterations -.. image:: images/sioux_falls_frank-wolfe-500_iter.png - :width: 590 - :alt: Sioux Falls Frank-Wolfe 500 iterations -.. image:: images/sioux_falls_cfw-500_iter.png - :width: 590 - :alt: Sioux Falls Conjugate Frank-Wolfe 500 iterations -.. image:: images/sioux_falls_bfw-500_iter.png - :width: 590 - :alt: Sioux Falls Biconjugate Frank-Wolfe 500 iterations - -Anaheim -~~~~~~~ - -Network has: - -* Links: 914 -* Nodes: 416 -* Zones: 38 - -.. image:: images/anaheim_msa-500_iter.png - :width: 590 - :alt: Anaheim MSA 500 iterations -.. image:: images/anaheim_frank-wolfe-500_iter.png - :width: 590 - :alt: Anaheim Frank-Wolfe 500 iterations -.. image:: images/anaheim_cfw-500_iter.png - :width: 590 - :alt: Anaheim Conjugate Frank-Wolfe 500 iterations -.. image:: images/anaheim_bfw-500_iter.png - :width: 590 - :alt: Anaheim Biconjugate Frank-Wolfe 500 iterations - -Winnipeg -~~~~~~~~ - -Network has: - -* Links: 914 -* Nodes: 416 -* Zones: 38 - -.. image:: images/winnipeg_msa-500_iter.png - :width: 590 - :alt: Winnipeg MSA 500 iterations -.. image:: images/winnipeg_frank-wolfe-500_iter.png - :width: 590 - :alt: Winnipeg Frank-Wolfe 500 iterations -.. image:: images/winnipeg_cfw-500_iter.png - :width: 590 - :alt: Winnipeg Conjugate Frank-Wolfe 500 iterations -.. image:: images/winnipeg_bfw-500_iter.png - :width: 590 - :alt: Winnipeg Biconjugate Frank-Wolfe 500 iterations - -The results for Winnipeg do not seem extremely good when compared to a highly, -but we believe posting its results would suggest deeper investigation by one -of our users :-), - - -Barcelona -~~~~~~~~~ - -Network has: - -* Links: 2,522 -* Nodes: 1,020 -* Zones: 110 - -.. image:: images/barcelona_msa-500_iter.png - :width: 590 - :alt: Barcelona MSA 500 iterations -.. image:: images/barcelona_frank-wolfe-500_iter.png - :width: 590 - :alt: Barcelona Frank-Wolfe 500 iterations -.. image:: images/barcelona_cfw-500_iter.png - :width: 590 - :alt: Barcelona Conjugate Frank-Wolfe 500 iterations -.. image:: images/barcelona_bfw-500_iter.png - :width: 590 - :alt: Barcelona Biconjugate Frank-Wolfe 500 iterations - -Chicago Regional -~~~~~~~~~~~~~~~~ - -Network has: - -* Links: 39,018 -* Nodes: 12,982 -* Zones: 1,790 - -.. image:: images/chicago_regional_msa-500_iter.png - :width: 590 - :alt: Chicago MSA 500 iterations -.. image:: images/chicago_regional_frank-wolfe-500_iter.png - :width: 590 - :alt: Chicago Frank-Wolfe 500 iterations -.. image:: images/chicago_regional_cfw-500_iter.png - :width: 590 - :alt: Chicago Conjugate Frank-Wolfe 500 iterations -.. image:: images/chicago_regional_bfw-500_iter.png - :width: 590 - :alt: Chicago Biconjugate Frank-Wolfe 500 iterations - -Convergence Study ------------------ - -Besides validating the final results from the algorithms, we have also compared -how well they converge for the largest instance we have tested (Chicago -Regional), as that instance has a comparable size to real-world models. - -.. image:: images/convergence_comparison.png - :width: 590 - :alt: Algorithm convergence comparison - -Not surprinsingly, one can see that Frank-Wolfe far outperforms the Method of -Successive Averages for a number of iterations larger than 25, and is capable of -reaching 1.0e-04 just after 800 iterations, while MSA is still at 3.5e-4 even -after 1,000 iterations. - -The actual show, however, is left for the Biconjugate Frank-Wolfe -implementation, which delivers a relative gap of under 1.0e-04 in under 200 -iterations, and a relative gap of under 1.0e-05 in just over 700 iterations. - -This convergence capability, allied to its computational performance described -below suggest that AequilibraE is ready to be used in large real-world -applications. - -Computational performance -------------------------- -Running on a Thinkpad X1 extreme equipped with a 6 cores 8750H CPU and 32Gb of -2667Hz RAM, AequilibraE performed 1,000 iterations of Frank-Wolfe assignment -on the Chicago Network in just under 46 minutes, while Biconjugate Frank Wolfe -takes just under 47 minutes. - -During this process, the sustained CPU clock fluctuated between 3.05 and 3.2GHz -due to the laptop's thermal constraints, suggesting that performance in modern -desktops would be better - -Noteworthy items ----------------- - -.. note:: - The biggest opportunity for performance in AequilibraE right now it to apply - network contraction hierarchies to the building of the graph, but that is - still a long-term goal - diff --git a/docs/source/usageexamples.rst b/docs/source/usageexamples.rst index bda851498..15c89fbe2 100644 --- a/docs/source/usageexamples.rst +++ b/docs/source/usageexamples.rst @@ -88,1135 +88,3 @@ Each instance contains the following folder structure and contents: * Composite scenario comparison flow map (gray is flow maintained in both scenarios, red is flow growth and green is flow decline) - -Comprehensive example ---------------------- - -The process of generating the data provided in the sample data above from the -data downloaded from the TNTP instances was similar than a natural workflow one -would find in a traditional model, and it was developed as a Jupyter notebook, -which is available on -`Github `_ - -Below we have that same workflow as a single script - -:: - - import sys - from os.path import join - import numpy as np - from math import log10, floor - import matplotlib.pyplot as plt - from aequilibrae.distribution import GravityCalibration, Ipf, GravityApplication, SyntheticGravityModel - from aequilibrae import Parameters - from aequilibrae.project import Project - from aequilibrae.paths import PathResults, SkimResults - from aequilibrae.matrix import AequilibraeData, AequilibraeMatrix - from aequilibrae import logger - from aequilibrae.paths import TrafficAssignment, TrafficClass - - import logging - - ######### FILES AND FOLDER ######### - - fldr = 'D:/release/Sample models/sioux_falls_2020_02_15' - proj_name = 'SiouxFalls.sqlite' - - # remove the comments for the lines below to run the Chicago model example instead - # fldr = 'D:/release/Sample models/Chicago_2020_02_15' - - dt_fldr = '0_tntp_data' - prj_fldr = '1_project' - skm_fldr = '2_skim_results' - assg_fldr = '4_assignment_results' - dstr_fldr = '5_distribution_results' - frcst_fldr = '6_forecast' - ftr_fldr = '7_future_year_assignment' - - ########### LOGGING ################# - - p = Parameters() - p.parameters['system']['logging_directory'] = fldr - p.write_back() - # To make sure the logging will go where it should, stop the script here and - # re-run it - - # Because assignment takes a long time, we want the log to be shown here - stdout_handler = logging.StreamHandler(sys.stdout) - formatter = logging.Formatter("%(asctime)s;%(name)s;%(levelname)s ; %(message)s") - stdout_handler.setFormatter(formatter) - logger.addHandler(stdout_handler) - - ########### PROJECT ################# - - project = Project() - project.load(join(fldr, prj_fldr)) - - ########### PATH COMPUTATION ################# - - # we build all graphs - project.network.build_graphs() - # We get warnings that several fields in the project are filled with NaNs. Which is true, but we won't - # use those fields - - # we grab the graph for cars - graph = project.network.graphs['c'] - - # let's say we want to minimize distance - graph.set_graph('distance') - - # And will skim time and distance while we are at it - graph.set_skimming(['free_flow_time', 'distance']) - - # And we will allow paths to be compute going through other centroids/centroid connectors - # required for the Sioux Falls network, as all nodes are centroids - graph.set_blocked_centroid_flows(False) - - # instantiate a path results object and prepare it to work with the graph - res = PathResults() - res.prepare(graph) - - # compute a path from node 2 to 13 - res.compute_path(2, 13) - - # We can get the sequence of nodes we traverse - res.path_nodes - - # We can get the link sequence we traverse - res.path - - # We can get the mileposts for our sequence of nodes - res.milepost - - # And We can the skims for our tree - res.skims - - # If we want to compute the path for a different destination and same origin, we can just do this - # It is way faster when you have large networks - res.update_trace(4) - - ########## SKIMMING ################### - - - # setup the object result - res = SkimResults() - res.prepare(graph) - - # And run the skimming - res.compute_skims() - - # The result is an AequilibraEMatrix object - skims = res.skims - - # We can export to AEM and OMX - skims.export(join(fldr, skm_fldr, 'skimming_on_time.aem')) - skims.export(join(fldr, skm_fldr, 'skimming_on_time.omx')) - - ######### TRAFFIC ASSIGNMENT WITH SKIMMING - - demand = AequilibraeMatrix() - demand.load(join(fldr, dt_fldr, 'demand.omx')) - demand.computational_view(['matrix']) # We will only assign one user class stored as 'matrix' inside the OMX file - - assig = TrafficAssignment() - - # Creates the assignment class - assigclass = TrafficClass(graph, demand) - - # The first thing to do is to add at list of traffic classes to be assigned - assig.set_classes([assigclass]) - - assig.set_vdf("BPR") # This is not case-sensitive # Then we set the volume delay function - - assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) # And its parameters - - assig.set_capacity_field("capacity") # The capacity and free flow travel times as they exist in the graph - assig.set_time_field("free_flow_time") - - # And the algorithm we want to use to assign - assig.set_algorithm('bfw') - - # since I haven't checked the parameters file, let's make sure convergence criteria is good - assig.max_iter = 1000 - assig.rgap_target = 0.00001 - - assig.execute() # we then execute the assignment - - # Convergence report is easy to see - import pandas as pd - convergence_report = pd.DataFrame(assig.assignment.convergence_report) - convergence_report.head() - - # The link flows are easy to export. - # we do so for csv and AequilibraEData - assigclass.results.save_to_disk(join(fldr, assg_fldr, 'link_flows_c.csv'), output="loads") - assigclass.results.save_to_disk(join(fldr, assg_fldr, 'link_flows_c.aed'), output="loads") - - # the skims are easy to get. - - # The blended one are here - avg_skims = assigclass.results.skims - - # The ones for the last iteration are here - last_skims = assigclass._aon_results.skims - - # Assembling a single final skim file can be done like this - # We will want only the time for the last iteration and the distance averaged out for all iterations - kwargs = {'file_name': join(fldr, assg_fldr, 'skims.aem'), - 'zones': graph.num_zones, - 'matrix_names': ['time_final', 'distance_blended']} - - # Create the matrix file - out_skims = AequilibraeMatrix() - out_skims.create_empty(**kwargs) - out_skims.index[:] = avg_skims.index[:] - - # Transfer the data - # The names of the skims are the name of the fields - out_skims.matrix['time_final'][:, :] = last_skims.matrix['free_flow_time'][:, :] - # It is CRITICAL to assign the matrix values using the [:,:] - out_skims.matrix['distance_blended'][:, :] = avg_skims.matrix['distance'][:, :] - - out_skims.matrices.flush() # Make sure that all data went to the disk - - # Export to OMX as well - out_skims.export(join(fldr, assg_fldr, 'skims.omx')) - - ############# TRIP DISTRIBUTION ################# - - # The demand is already in memory - - # Need the skims - imped = AequilibraeMatrix() - imped.load(join(fldr, assg_fldr, 'skims.aem')) - - # But before using the data, let's get some impedance for the intrazonals - # Let's assume it is 75% of the closest zone - - # If we run the code below more than once, we will be overwriting the diagonal values with non-sensical data - # so let's zero it first - np.fill_diagonal(imped.matrix['time_final'], 0) - - # We compute it with a little bit of NumPy magic - intrazonals = np.amin(imped.matrix['time_final'], where=imped.matrix['time_final'] > 0, - initial=imped.matrix['time_final'].max(), axis=1) - intrazonals *= 0.75 - - # Then we fill in the impedance matrix - np.fill_diagonal(imped.matrix['time_final'], intrazonals) - - # We set the matrices for use in computation - imped.computational_view(['time_final']) - demand.computational_view(['matrix']) - - - # Little function to plot TLFDs - def plot_tlfd(demand, skim, name): - # No science here. Just found it works well for Sioux Falls & Chicago - b = floor(log10(skim.shape[0]) * 10) - n, bins, patches = plt.hist(np.nan_to_num(skim.flatten(), 0), bins=b, - weights=np.nan_to_num(demand.flatten()), - density=False, facecolor='g', alpha=0.75) - - plt.xlabel('Trip length') - plt.ylabel('Probability') - plt.title('Trip-length frequency distribution') - plt.savefig(name, format="png") - plt.clf() - - - # Calibrate models with the two functional forms - for function in ['power', 'expo']: - model = GravityCalibration(matrix=demand, impedance=imped, function=function, nan_as_zero=True) - model.calibrate() - - # we save the model - model.model.save(join(fldr, dstr_fldr, f'{function}_model.mod')) - - # We save a trip length frequency distribution image - plot_tlfd(model.result_matrix.matrix_view, imped.matrix_view, - join(fldr, dstr_fldr, f'{function}_tfld.png')) - - # We can save the result of applying the model as well - # we can also save the calibration report - with open(join(fldr, dstr_fldr, f'{function}_convergence.log'), 'w') as otp: - for r in model.report: - otp.write(r + '\n') - - # We save a trip length frequency distribution image - plot_tlfd(demand.matrix_view, imped.matrix_view, join(fldr, dstr_fldr, 'demand_tfld.png')) - - ################ FORECAST ############################# - - # We compute the vectors from our matrix - mat = AequilibraeMatrix() - - mat.load(join(fldr, dt_fldr, 'demand.omx')) - mat.computational_view() - origins = np.sum(mat.matrix_view, axis=1) - destinations = np.sum(mat.matrix_view, axis=0) - - args = {'file_path':join(fldr, frcst_fldr, 'synthetic_future_vector.aed'), - "entries": mat.zones, - "field_names": ["origins", "destinations"], - "data_types": [np.float64, np.float64], - "memory_mode": False} - - vectors = AequilibraeData() - vectors.create_empty(**args) - - vectors.index[:] =mat.index[:] - - # Then grow them with some random growth between 0 and 10% - Plus balance them - vectors.origins[:] = origins * (1+ np.random.rand(vectors.entries)/10) - vectors.destinations[:] = destinations * (1+ np.random.rand(vectors.entries)/10) - vectors.destinations *= vectors.origins.sum()/vectors.destinations.sum() - - # Impedance matrix is already in memory - - # We want the main diagonal to be zero, as the original matrix does - # not have intrazonal trips - np.fill_diagonal(imped.matrix_view, np.nan) - - # Apply the gravity models - for function in ['power', 'expo']: - model = SyntheticGravityModel() - model.load(join(fldr, dstr_fldr, f'{function}_model.mod')) - - outmatrix = join(fldr,frcst_fldr, f'demand_{function}_model.aem') - apply = GravityApplication() - args = {"impedance": imped, - "rows": vectors, - "row_field": "origins", - "model": model, - "columns": vectors, - "column_field": "destinations", - "output": outmatrix, - "nan_as_zero":True - } - - gravity = GravityApplication(**args) - gravity.apply() - - #We get the output matrix and save it to OMX too - resm = AequilibraeMatrix() - resm.load(outmatrix) - resm.export(join(fldr,frcst_fldr, f'demand_{function}_model.omx')) - - # APPLY IPF - demand = AequilibraeMatrix() - demand.load(join(fldr, dt_fldr, 'demand.omx')) - demand.computational_view() - - args = {'matrix': demand, - 'rows': vectors, - 'columns': vectors, - 'column_field': "destinations", - 'row_field': "origins", - 'nan_as_zero': True} - - ipf = Ipf(**args) - ipf.fit() - - output = AequilibraeMatrix() - output.load(ipf.output.file_path) - - output.export(join(fldr,frcst_fldr, 'demand_ipf.aem')) - output.export(join(fldr,frcst_fldr, 'demand_ipf.omx')) - - - logger.info('\n\n\n TRAFFIC ASSIGNMENT FOR FUTURE YEAR') - - # Let's use the IPF matrix - demand = AequilibraeMatrix() - demand.load(join(fldr, frcst_fldr, 'demand_ipf.omx')) - demand.computational_view() # There is only one matrix there, so don;t even worry about its core name - - assig = TrafficAssignment() - - # Creates the assignment class - assigclass = TrafficClass(graph, demand) - - # The first thing to do is to add at list of traffic classes to be assigned - assig.set_classes([assigclass]) - - assig.set_vdf("BPR") # This is not case-sensitive # Then we set the volume delay function - - assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) # And its parameters - - assig.set_capacity_field("capacity") # The capacity and free flow travel times as they exist in the graph - assig.set_time_field("free_flow_time") - - # And the algorithm we want to use to assign - assig.set_algorithm('bfw') - - # since I haven't checked the parameters file, let's make sure convergence criteria is good - assig.max_iter = 1000 - assig.rgap_target = 0.00001 - - assig.execute() # we then execute the assignment - - -.. _example_logging: - -Logging -------- -AequilibraE uses Python's standard logging library to a file called -*aequilibrae.log*, but the output folder for this logging can be changed to a -custom system folder by altering the parameter **system --> logging_directory** on -the parameters file. - -As an example, one could do programatically change the output folder to -*'D:/myProject/logs'* by doing the following: - -:: - - from aequilibrae import Parameters - - fldr = 'D:/myProject/logs' - - p = Parameters() - p.parameters['system']['logging_directory'] = fldr - p.write_back() - -The other useful resource, especially during model debugging it to also show -all log messages directly on the screen. Doing that requires a little knowledge -of the Python Logging library, but it is just as easy: - -:: - - from aequilibrae import logger - import logging - - stdout_handler = logging.StreamHandler(sys.stdout) - logger.addHandler(stdout_handler) - -.. _example_usage_parameters: - -Parameters module ------------------ -Several examples on how to manipulate the parameters within AequilibraE have -been shown in other parts of this tutorial. - -However, in case you ever have trouble with parameter changes you have made, -you can always revert them to their default values. But remember, **ALL** -**CHANGES WILL BE LOST**. - -:: - - from aequilibrae import Parameters - - fldr = 'D:/myProject/logs' - - p = Parameters() - p.reset_default() - - -.. _example_usage_matrix: - -Matrix module -------------- - -Let's see two cases where we work with the matrix module - -Extracting vectors -~~~~~~~~~~~~~~~~~~ - -Let's extract the vectors for total origins and destinations for the Chicago -model demand matrix: - -:: - - from aequilibrae.matrix import AequilibraeData, AequilibraeMatrix - import numpy as np - - mat = AequilibraeMatrix() - mat.load("D:/release/Sample models/Chicago_2020_02_15/demand.omx") - m = mat.get_matrix("matrix") - - vectors = "D:/release/Sample models/Chicago_2020_02_15/vectors.aed" - args = { - "file_path": vectors, - "entries": vec_1.shape[0], - "field_names": ["origins", "destinations"], - "data_types": [np.float64, np.float64], - } - dataset = AequilibraeData() - dataset.create_empty(**args) - - # Transfer the data - dataset.index[:] =mat.index[:] - dataset.origins[:] = np.sum(m, axis=1)[:] - dataset.destinations[:] = np.sum(m, axis=0)[:] - -Comprehensive example -~~~~~~~~~~~~~~~~~~~~~ - -Lets say we want to Import the freight matrices provided with FAF into AequilibraE's matrix format -in order to create some Delaunay Lines in QGIS or to perform traffic assignment - -Required data -+++++++++++++ - -* `FAF Matrices `__ -* `Zones System `__ - -Useful Information -++++++++++++++++++ - -* `FAF overview `__ -* `FAF User Guide `__ -* `The blog post (with data) `__ - -The code -++++++++ - -We import all libraries we will need, including the AequilibraE - -:: - - import pandas as pd - import numpy as np - import os - from aequilibrae.matrix import AequilibraeMatrix - from scipy.sparse import coo_matrix - -Now we set all the paths for files and parameters we need and import the matrices into a Pandas DataFrame - -:: - - data_folder = 'Y:/ALL DATA/DATA/Pedro/Professional/Data/USA/FAF/4.4' - data_file = 'FAF4.4_HiLoForecasts.csv' - sctg_names_file = 'sctg_codes.csv' # Simplified to 50 characters, which is AequilibraE's limit - output_folder = data_folder - - matrices = pd.read_csv(os.path.join(data_folder, data_file), low_memory=False) - -We import the sctg codes - -:: - - sctg_names = pd.read_csv(os.path.join(data_folder, sctg_names_file), low_memory=False) - sctg_names.set_index('Code', inplace=True) - sctg_descr = list(sctg_names['Commodity Description']) - - -We now process the matrices to collect all the data we need, such as: - -* List of zones -* CSTG codes -* Matrices/scenarios we are importing - -:: - - all_zones = np.array(sorted(list(set( list(matrices.dms_orig.unique()) + list(matrices.dms_dest.unique()))))) - - # Count them and create a 0-based index - num_zones = all_zones.shape[0] - idx = np.arange(num_zones) - - # Creates the indexing dataframes - origs = pd.DataFrame({"from_index": all_zones, "from":idx}) - dests = pd.DataFrame({"to_index": all_zones, "to":idx}) - - # adds the new index columns to the pandas dataframe - matrices = matrices.merge(origs, left_on='dms_orig', right_on='from_index', how='left') - matrices = matrices.merge(dests, left_on='dms_dest', right_on='to_index', how='left') - - # Lists sctg codes and all the years/scenarios we have matrices for - mat_years = [x for x in matrices.columns if 'tons' in x] - sctg_codes = matrices.sctg2.unique() - -We now import one matrix for each year, saving all the SCTG codes as different matrix cores in our zoning system - -:: - - # aggregate the matrix according to the relevant criteria - agg_matrix = matrices.groupby(['from', 'to', 'sctg2'])[mat_years].sum() - - # returns the indices - agg_matrix.reset_index(inplace=True) - - - for y in mat_years: - mat = AequilibraeMatrix() - - # Here it does not make sense to use OMX - # If one wants to create an OMX from other data sources, openmatrix is - # the library to use - kwargs = {'file_name': os.path.join(output_folder, y + '.aem'), - 'zones': num_zones, - 'matrix_names': sctg_descr} - - mat.create_empty(**kwargs) - mat.index[:] = all_zones[:] - # for all sctg codes - for i in sctg_names.index: - prod_name = sctg_names['Commodity Description'][i] - mat_filtered_sctg = agg_matrix[agg_matrix.sctg2 == i] - - m = coo_matrix((mat_filtered_sctg[y], (mat_filtered_sctg['from'], mat_filtered_sctg['to'])), - shape=(num_zones, num_zones)).toarray().astype(np.float64) - - mat.matrix[prod_name][:,:] = m[:,:] - - mat.close() - - -.. _example_usage_project: - -Project module --------------- - -Let's suppose one wants to create project files for a list of 5 cities around -the world with their complete networks downloaded from -`Open Street Maps `_ and place them on a local -folder for analysis at a later time. - -There are few important parameters regarding the use of OSM Overpass servers -that one needs to pay attention to: - -* Overpass API endpoint -* Maximum query area (m\ :sup:`2`) -* Sleep time (between successive queries when the queried area is too large) - -The lines regarding parameters in the code below assume that you have a local -instance of the Overpass server installed and can overload it with unlimited -queries in rapid succession. For more details see :ref:`parameters_osm`. - -:: - - from aequilibrae import Project, Parameters - - cities = ["Darwin, Australia", - "Karlsruhe, Germany", - "London, UK", - "Paris, France", - "Auckland, New Zealand"] - - for city in cities: - print(city) - pth = f'd:/net_tests/{city}.sqlite' - - p = Project() - p.new(pth) - - # Set parameters for a local private Overpass API server - par = Parameters() - par.parameters['osm']['overpass_endpoint'] = "http://192.168.0.110:32780/api" - par.parameters['osm']['max_query_area_size'] = 10000000000 - par.parameters['osm']['sleeptime'] = 0 - par.write_back() - - p.network.create_from_osm(place_name=city) - p.conn.close() - del p - - - -If one wants to load a project and check some of its properties, it is easy: - -:: - - >>> from aequilibrae.project import Project - - >>> p = Project() - >>> p.open('path/to_project_folder') - - # for the modes available in the model - >>> p.network.modes() - ['car', 'walk', 'bicycle'] - - >>> p.network.count_links() - 157926 - - >>> p.network.count_nodes() - 793200 - - -.. _example_usage_paths: - -Paths module ------------- - -:: - - from aequilibrae.paths import allOrNothing - from aequilibrae.paths import path_computation - from aequilibrae.paths.results import AssignmentResults as asgr - from aequilibrae.paths.results import PathResults as pthr - -Path computation -~~~~~~~~~~~~~~~~ - -Skimming -~~~~~~~~ - -Let's suppose you want to compute travel times between all zone on your network. In that case, -you need only a graph that you have previously built, and the list of skims you want to compute. - -:: - - from aequilibrae.paths.results import SkimResults as skmr - from aequilibrae.paths import Graph - from aequilibrae.paths import NetworkSkimming - - # We instantiate the graph and load it from disk (say you created it using the QGIS GUI - g = Graph() - g.load_from_disk(aeg_pth) - - # You now have to set the graph for what you want - # In this case, we are computing fastest path (minimizing free flow time) - g.set_graph(cost_field='fftime') - - # We are also **blocking** paths from going through centroids - g.set_blocked_centroid_flows(block_centroid_flows=True) - - # We will be skimming for fftime **AND** distance along the way - g.set_skimming(['fftime', 'distance']) - - # We instantiate the skim results and prepare it to have results compatible with the graph provided - result = skmr() - result.prepare(g) - - # We create the network skimming object and execute it - # This is multi-threaded, so if the network is too big, prepare for a slow computer - skm = NetworkSkimming(g, result) - skm.execute() - - -If you want to use fewer cores for this computation (which also saves memory), you also can do it -You just need to use the method *set_cores* before you run the skimming. Ideally it is done before preparing it - -:: - - result = skmr() - result.set_cores(3) - result.prepare(g) - -And if you want to compute skims between all nodes in the network, all you need to do is to make sure -the list of centroids in your graph is updated to include all nodes in the graph - -:: - - from aequilibrae.paths.results import SkimResults as skmr - from aequilibrae.paths import Graph - from aequilibrae.paths import NetworkSkimming - - g = Graph() - g.load_from_disk(aeg_pth) - - # Let's keep the original list of centroids in case we want to use it again - orig_centr = g.centroids - - # Now we set the list of centroids to include all nodes in the network - g.prepare_graph(g.all_nodes) - - # And continue **almost** like we did before - # We just need to remember to NOT block paths through centroids. Otherwise there will be no paths available - g.set_graph(cost_field='fftime', block_centroid_flows=False) - g.set_skimming('fftime') - - result = skmr() - result.prepare(g) - - skm = NetworkSkimming(g, result) - skm.execute() - -Setting skimming after setting the graph is **CRITICAL**, and the skim matrices are part of the result object. - -You can save the results to your place of choice in AequilibraE format or export to OMX or CSV - -:: - - result.skims.export('path/to/desired/folder/file_name.omx') - - result.skims.export('path/to/desired/folder/file_name.csv') - - result.skims.copy('path/to/desired/folder/file_name.aem') - -.. _comprehensive_traffic_assignment_case: - -Traffic assignment -~~~~~~~~~~~~~~~~~~ - -A comprehensive example of assignment - -:: - - from aequilibrae.project import Project - from aequilibrae.paths import TrafficAssignment, TrafficClass - from aequilibrae.matrix import AequilibraeMatrix - - assig = TrafficAssignment() - - proj = Project() - proj.load('path/to/folder/SiouxFalls.sqlite') - proj.network.build_graphs() - # Mode c is car - car_graph = proj.network.graphs['c'] - - # If, for any reason, you would like to remove a set of links from the - # graph based solely on the modes assigned to links in the project file - # This will alter the Graph ID, but everything else (cost field, set of - # centroids and configuration for blocking flows through centroid connectors - # remains unaltered - car_graph.exclude_links([123, 451, 1, 569, 345]) - - mat = AequilibraeMatrix() - mat.load('path/to/folder/demand.omx') - # We will only assign one user class stored as 'matrix' inside the OMX file - mat.computational_view(['matrix']) - - # Creates the assignment class - assigclass = TrafficClass(g, mat) - - # If you want to know which assignment algorithms are available: - assig.algorithms_available() - - # If you want to know which Volume-Delay functions are available - assig.vdf.functions_available() - - # The first thing to do is to add at list of traffic classes to be assigned - assig.set_classes([assigclass]) - - # Then we set the volume delay function - assig.set_vdf("BPR") # This is not case-sensitive - - # And its parameters - assig.set_vdf_parameters({"alpha": "alpha", "beta": "beta"}) - - # If you don't have parameters in the network, but rather global ones - # assig.set_vdf_parameters({"alpha": 0.15, "beta": 4}) - - # The capacity and free flow travel times as they exist in the graph - assig.set_capacity_field("capacity") - assig.set_time_field("free_flow_time") - - # And the algorithm we want to use to assign - assig.set_algorithm('bfw') - - # To overwrite the number of iterations and the relative gap intended - assig.max_iter = 250 - assig.rgap_target = 0.0001 - - # To overwrite the number of CPU cores to be used - assig.set_cores(3) - - # we then execute the assignment - assig.execute() - -Assigning traffic on TNTP instances -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There is a set of well known traffic assignment problems used in the literature -maintained on `Ben's GitHub `_ -that is often used for tests, so we will use one of those problems here. - -Let's suppose we want to perform traffic assignment for one of those problems -and check the results against the reference results. - -The parsing and importing of those networks are not really the case here, but -there is `online code `_ -available for doing that work. - -:: - - import os - import sys - import numpy as np - import pandas as pd - from aequilibrae.paths import TrafficAssignment - from aequilibrae.paths import Graph - from aequilibrae.paths.traffic_class import TrafficClass - from aequilibrae.matrix import AequilibraeMatrix, AequilibraeData - import matplotlib.pyplot as plt - - from aequilibrae import logger - import logging - - # We redirect the logging output to the terminal - stdout_handler = logging.StreamHandler(sys.stdout) - logger.addHandler(stdout_handler) - - # Let's work with Sioux Falls - os.chdir('D:/src/TransportationNetworks/SiouxFalls') - result_file = 'SiouxFalls_flow.tntp' - - # Loads and prepares the graph - g = Graph() - g.load_from_disk('graph.aeg') - g.set_graph('time') - g.cost = np.array(g.cost, copy=True) - g.set_skimming(['time']) - g.set_blocked_centroid_flows(True) - - # Loads and prepares the matrix - mat = AequilibraeMatrix() - mat.load('demand.aem') - mat.computational_view(['matrix']) - - # Creates the assignment class - assigclass = TrafficClass(g, mat) - - # Instantiates the traffic assignment problem - assig = TrafficAssignment() - - # configures it properly - assig.set_vdf('BPR') - assig.set_vdf_parameters(**{'alpha': 0.15, 'beta': 4.0}) - assig.set_capacity_field('capacity') - assig.set_time_field('time') - assig.set_classes(assigclass) - # could be assig.set_algorithm('frank-wolfe') - assig.set_algorithm('msa') - - # Execute the assignment - assig.execute() - - # the results are within each traffic class only one, in this case - assigclass.results.link_loads - -.. _multiple_user_classes: - -Setting multiple user classes before assignment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Let's suppose one wants to setup a matrix for assignment that has two user -classes, *red_cars* and *blue cars* for a single traffic class. To do that, one -needs only to call the *computational_view* method with a list of the two -matrices of interest. Both matrices need to be contained in the same file (and -to be contiguous if an * .aem instead of a * .omx file) however. - -:: - - mat = AequilibraeMatrix() - mat.load('demand.aem') - mat.computational_view(['red_cars', 'blue_cars']) - - -Advanced usage: Building a Graph -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Let's suppose now that you are interested in creating links from a bespoke procedure. For -the purpose of this example, let's say you have a sparse matrix representing a graph as -an adjacency matrix - -:: - - from aequilibrae.paths import Graph - from aequilibrae.project.network import Network - from scipy.sparse import coo_matrix - - # original_adjacency_matrix is a sparse matrix where positive values are actual links - # where the value of the cell is the distance in that link - - # We create the sparse matrix in proper sparse matrix format - sparse_graph = coo_matrix(original_adjacency_matrix) - - # We create the structure to create the network - all_types = [k._Graph__integer_type, - k._Graph__integer_type, - k._Graph__integer_type, - np.int8, - k._Graph__float_type, - k._Graph__float_type] - - # List of all required link fields for a network - # Network.req_link_flds - - # List of all required node fields for a network - # Network.req_node_flds - - # List of fields that are reserved for internal workings - # Network.protected_fields - - dt = [(t, d) for t, d in zip(all_titles, all_types)] - - # Number of links - num_links = sparse_graph.data.shape[0] - - my_graph = Graph() - my_graph.network = np.zeros(links, dtype=dt) - - my_graph.network['link_id'] = np.arange(links) + 1 - my_graph.network['a_node'] = sparse_graph.row - my_graph.network['b_node'] = sparse_graph.col - my_graph.network["distance"] = sparse_graph.data - - # If the links are directed (from A to B), direction is 1. If bi-directional, use zeros - my_graph.network['direction'] = np.ones(links) - - # Let's say that all nodes in the network are centroids - list_of_centroids = np.arange(max(sparse_graph.shape[0], sparse_graph.shape[0])+ 1) - centroids_list = np.array(list_of_centroids) - - my_graph.type_loaded = 'NETWORK' - my_graph.status = 'OK' - my_graph.network_ok = True - my_graph.prepare_graph(centroids_list) - -This usage is really advanced, and very rarely not-necessary. Make sure to know what you are doing -before going down this route - -.. _example_usage_distribution: - -Trip distribution ------------------ - -The support for trip distribution in AequilibraE is not very comprehensive, -mostly because of the loss of relevance that such type of model has suffered -in the last decade. - -However, it is possible to calibrate and apply synthetic gravity models and -to perform Iterative Proportional Fitting (IPF) with really high performance, -which might be of use in many applications other than traditional distribution. - - -.. Synthetic gravity calibration -.. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. :: - -.. some code - -Synthetic gravity application -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In this example, imagine that you have your demographic information in an -sqlite database and that you have already computed your skim matrix. - -It is also important to notice that it is crucial to have consistent data, such -as same set of zones (indices) in both the demographics and the impedance -matrix. - -:: - - import pandas as pd - import sqlite3 - - from aequilibrae.matrix import AequilibraeMatrix - from aequilibrae.matrix import AequilibraeData - - from aequilibrae.distribution import SyntheticGravityModel - from aequilibrae.distribution import GravityApplication - - - # 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" - - # 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('path/to/impedance_matrix.aem') - matrix.computational_view(['distance']) - - # We create the vectors we will use - conn = sqlite3.connect('path/to/demographics/database') - query = "SELECT zone_id, population, employment FROM demographics;" - df = pd.read_sql_query(query,conn) - - index = df.zone_id.values[:] - zones = index.shape[0] - - # You create the vectors you would have - df = df.assign(production=df.population * 3.0) - df = df.assign(attraction=df.employment * 4.0) - - # We create the vector database - args = {"entries": zones, "field_names": ["productions", "attractions"], - "data_types": [np.float64, np.float64], "memory_mode": True} - vectors = AequilibraeData() - vectors.create_empty(**args) - - # Assign the data to the vector object - vectors.productions[:] = df.production.values[:] - vectors.attractions[:] = df.attraction.values[:] - vectors.index[:] = zones[:] - - # Balance the vectors - vectors.attractions[:] *= vectors.productions.sum() / vectors.attractions.sum() - - args = {"impedance": matrix, - "rows": vectors, - "row_field": "productions", - "model": model, - "columns": vectors, - "column_field": "attractions", - "output": 'path/to/output/matrix.aem', - "nan_as_zero":True - } - - gravity = GravityApplication(**args) - gravity.apply() - -Iterative Proportional Fitting (IPF) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The implementation of IPF is fully vectorized and leverages all the speed of NumPy, but it does not include the -fancy multithreading implemented in path computation. - -**Please note that the AequilibraE matrix used as input is OVERWRITTEN by the IPF** - -:: - - import pandas as pd - from aequilibrae.distribution import Ipf - from aequilibrae.matrix import AequilibraeMatrix - from aequilibrae.matrix import AequilibraeData - - matrix = AequilibraeMatrix() - - # Here we can create from OMX or load from an AequilibraE matrix. - matrix.create_from_omx(path/to/aequilibrae_matrix, path/to/omxfile) - - # The matrix will be operated one (see the note on overwriting), so it does - # not make sense load an OMX matrix - - - source_vectors = pd.read_csv(path/to/CSVs) - zones = source_vectors.zone.shape[0] - - args = {"entries": zones, "field_names": ["productions", "attractions"], - "data_types": [np.float64, np.float64], "memory_mode": True} - - vectors = AequilibraEData() - vectors.create_empty(**args) - - vectors.productions[:] = source_vectors.productions[:] - vectors.attractions[:] = source_vectors.attractions[:] - - # We assume that the indices would be sorted and that they would match the matrix indices - vectors.index[:] = source_vectors.zones[:] - - args = { - "matrix": matrix, "rows": vectors, "row_field": "productions", "columns": vectors, - "column_field": "attractions", "nan_as_zero": False} - - fratar = Ipf(**args) - fratar.fit() - - # We can get back to our OMX matrix in the end - matrix.export(path/to_omx/output) - -.. Transit -.. ------- - -We only have import for now, and it is likely to not work on Windows if you want the geometries - -.. _example_usage_transit: - -.. GTFS import -.. ~~~~~~~~~~~ - -.. :: - -.. some code diff --git a/docs/source/validation.rst b/docs/source/validation.rst new file mode 100644 index 000000000..e0f454956 --- /dev/null +++ b/docs/source/validation.rst @@ -0,0 +1,13 @@ +Validation & Benchmarking +========================= + +AequilibraE's main objective is to be a fully-fledged modeling platform, and +therefore most of its features are geared towards that. In time, this section +of the documentation will have more detailed documentation, as it is the case +of Traffic Assignment, linked below. + +.. toctree:: + :maxdepth: 1 + + validation_benchmarking/traffic_assignment + validation_benchmarking/ipf_performance \ No newline at end of file diff --git a/docs/source/validation_benchmarking/ipf_performance.rst b/docs/source/validation_benchmarking/ipf_performance.rst new file mode 100644 index 000000000..5ab5192a6 --- /dev/null +++ b/docs/source/validation_benchmarking/ipf_performance.rst @@ -0,0 +1,147 @@ +IPF Performance +=============== + +It is quite common to have zones with different growth rates. To improve obtaining +a trip matrix, which satisfies both trip-end constraints, we can use iterative methods, +such as the iterative proportional fitting (IPF). In this section, we compare the +runtime of AquilibraE's current implementation of IPF, +with a general IPF algorithm, available `here `_. + +The figure below compares the :ref:`AequilibraE's IPF runtime` with one core with the benchmark Python +code. From the figure below, we can notice that the runtimes were practically the same for the +instances with 1,000 zones or less. As the number of zones increases, AequilibraE demonstrated to be faster +than the benchmark python code in instances with 1,000 < zones < 10,000, but it was a +slower than the benchmark for the larger instances with 10,000 and 15,000 zones. It's worth mentioning that +the user can set up a threshold for AequilibraE's IPF function, as well as use more than one +core to speed up the fitting process. + +.. image:: ../images/ipf_runtime_aequilibrae_vs_benchmark.png + :align: center + :alt: AequilibraE's IPF runtime + +As IPF is an embarassingly-parallel workload, we have implemented our version in Cython, taking full advantage +of parallelization and observing the impact of array orientation in memory. AequilibraE's +IPF allows the user to choose how many cores are used for IPF in order to speed up the fitting process, which +is extremely useful when handling models with lots of traffic zones. + +As we can see, instances with more zones benefited the most from the power of multi-processing +speeding up the runtime in barely five times using five cores. + +.. image:: ../images/ipf_runtime_vs_num_cores.png + :align: center + :alt: number of cores used in IPF + +These tests were ran on a TreadRipper 3970x workstation with 32 cores (64 threads) @ 3.7 GHz +and 256 Gb of RAM. With 32 cores in use, performing IPF took 0.105s on a 10,000 zones matrix, +and 0.224 seconds on a 15,000 matrix. The code is provided below for convenience + +.. _code-block-for-ipf-benchmarking: +.. code-block:: python + + # %% + from copy import deepcopy + from time import perf_counter + import numpy as np + import pandas as pd + from aequilibrae.distribution.ipf_core import ipf_core + from tqdm import tqdm + + # %% + # From: + # https://github.com/joshchea/python-tdm/blob/master/scripts/CalcDistribution.py + + def CalcFratar(ProdA, AttrA, Trips1, maxIter=10): + '''Calculates fratar trip distribution + ProdA = Production target as array + AttrA = Attraction target as array + Trips1 = Seed trip table for fratar + maxIter (optional) = maximum iterations, default is 10 + Returns fratared trip table + ''' + # print('Checking production, attraction balancing:') + sumP = ProdA.sum() + sumA = AttrA.sum() + # print('Production: ', sumP) + # print('Attraction: ', sumA) + if sumP != sumA: + # print('Productions and attractions do not balance, attractions will be scaled to productions!') + AttrA = AttrA*(sumP/sumA) + else: + pass + # print('Production, attraction balancing OK.') + # Run 2D balancing ---> + for balIter in range(0, maxIter): + ComputedProductions = Trips1.sum(1) + ComputedProductions[ComputedProductions == 0] = 1 + OrigFac = (ProdA/ComputedProductions) + Trips1 = Trips1*OrigFac[:, np.newaxis] + + ComputedAttractions = Trips1.sum(0) + ComputedAttractions[ComputedAttractions == 0] = 1 + DestFac = (AttrA/ComputedAttractions) + Trips1 = Trips1*DestFac + return Trips1 + + # %% + mat_sizes = [500, 750, 1000, 1500, 2500, 5000, 7500, 10000, 15000 + cores_to_use = [1, 2, 3, 4, 6, 8, 10, 12, 16, 20, 24, 28, 32] + + # %% + #Benchmarking + bench_data = [] + cores = 1 + repetitions = 3 + iterations = 100 + for zones in mat_sizes: + for repeat in tqdm(range(repetitions),f"Zone size: {zones}"): + mat1 = np.random.rand(zones, zones) + target_prod = np.random.rand(zones) + target_atra = np.random.rand(zones) + target_atra *= target_prod.sum()/target_atra.sum() + + aeq_mat = deepcopy(mat1) + t = perf_counter() + ipf_core(aeq_mat, target_prod, target_atra, max_iterations=iterations, tolerance=-5, cores=cores) + aeqt = perf_counter() - t + + bc_mat = deepcopy(mat1) + t = perf_counter() + x = CalcFratar(target_prod, target_atra, bc_mat, maxIter=iterations) + + bench_data.append([zones, perf_counter() - t, aeqt]) + + # %% + bench_df = pd.DataFrame(bench_data, columns=["zones", "PythonCode", "AequilibraE"]) + bench_df.groupby(["zones"]).mean().plot.bar() + + # %% + bench_df.groupby(["zones"]).mean() + + # %% + #Benchmarking + aeq_data = [] + repetitions = 1 + iterations = 50 + for zones in mat_sizes: + for cores in tqdm(cores_to_use,f"Zone size: {zones}"): + for repeat in range(repetitions): + mat1 = np.random.rand(zones, zones) + target_prod = np.random.rand(zones) + target_atra = np.random.rand(zones) + target_atra *= target_prod.sum()/target_atra.sum() + + aeq_mat = deepcopy(mat1) + t = perf_counter() + ipf_core(aeq_mat, target_prod, target_atra, max_iterations=iterations, tolerance=-5, cores=cores) + aeqt = perf_counter() - t + + aeq_data.append([zones, cores, aeqt]) + + # %% + aeq_df = pd.DataFrame(aeq_data, columns=["zones", "cores", "time"]) + aeq_df = aeq_df[aeq_df.zones>1000] + aeq_df = aeq_df.groupby(["zones", "cores"]).mean().reset_index() + aeq_df = aeq_df.pivot_table(index="zones", columns="cores", values="time") + for cores in cores_to_use[::-1]: + aeq_df.loc[:, cores] /= aeq_df[1] + aeq_df.transpose().plot() \ No newline at end of file diff --git a/docs/source/validation_benchmarking/traffic_assignment.rst b/docs/source/validation_benchmarking/traffic_assignment.rst new file mode 100644 index 000000000..2b416eb0a --- /dev/null +++ b/docs/source/validation_benchmarking/traffic_assignment.rst @@ -0,0 +1,322 @@ +.. _numerical_study_traffic_assignment: + +Traffic Assignment +================== + +Similar to other complex algorthms that handle a large amount of data through +complex computations, traffic assignment procedures can always be subject to at +least one very reasonable question: Are the results right? + +For this reason, we have used all equilibrium traffic assignment algorithms +available in AequilibraE to solve standard instances used in academia for +comparing algorithm results, some of which have are available with highly +converged solutions (~1e-14). Instances can be downloaded `here `_. + +Sioux Falls +----------- +Network has: + +* Links: 76 +* Nodes: 24 +* Zones: 24 + +.. image:: ../images/sioux_falls_msa-500_iter.png + :align: center + :width: 590 + :alt: Sioux Falls MSA 500 iterations +| +.. image:: ../images/sioux_falls_frank-wolfe-500_iter.png + :align: center + :width: 590 + :alt: Sioux Falls Frank-Wolfe 500 iterations +| +.. image:: ../images/sioux_falls_cfw-500_iter.png + :align: center + :width: 590 + :alt: Sioux Falls Conjugate Frank-Wolfe 500 iterations +| +.. image:: ../images/sioux_falls_bfw-500_iter.png + :align: center + :width: 590 + :alt: Sioux Falls Biconjugate Frank-Wolfe 500 iterations + +Anaheim +------- +Network has: + +* Links: 914 +* Nodes: 416 +* Zones: 38 + +.. image:: ../images/anaheim_msa-500_iter.png + :align: center + :width: 590 + :alt: Anaheim MSA 500 iterations +| +.. image:: ../images/anaheim_frank-wolfe-500_iter.png + :align: center + :width: 590 + :alt: Anaheim Frank-Wolfe 500 iterations +| +.. image:: ../images/anaheim_cfw-500_iter.png + :align: center + :width: 590 + :alt: Anaheim Conjugate Frank-Wolfe 500 iterations +| +.. image:: ../images/anaheim_bfw-500_iter.png + :align: center + :width: 590 + :alt: Anaheim Biconjugate Frank-Wolfe 500 iterations + +Winnipeg +-------- +Network has: + +* Links: 914 +* Nodes: 416 +* Zones: 38 + +.. image:: ../images/winnipeg_msa-500_iter.png + :align: center + :width: 590 + :alt: Winnipeg MSA 500 iterations +| +.. image:: ../images/winnipeg_frank-wolfe-500_iter.png + :align: center + :width: 590 + :alt: Winnipeg Frank-Wolfe 500 iterations +| +.. image:: ../images/winnipeg_cfw-500_iter.png + :align: center + :width: 590 + :alt: Winnipeg Conjugate Frank-Wolfe 500 iterations +| +.. image:: ../images/winnipeg_bfw-500_iter.png + :align: center + :width: 590 + :alt: Winnipeg Biconjugate Frank-Wolfe 500 iterations + +The results for Winnipeg do not seem extremely good when compared to a highly, +but we believe posting its results would suggest deeper investigation by one +of our users :-) + + +Barcelona +--------- +Network has: + +* Links: 2,522 +* Nodes: 1,020 +* Zones: 110 + +.. image:: ../images/barcelona_msa-500_iter.png + :align: center + :width: 590 + :alt: Barcelona MSA 500 iterations +| +.. image:: ../images/barcelona_frank-wolfe-500_iter.png + :align: center + :width: 590 + :alt: Barcelona Frank-Wolfe 500 iterations +| +.. image:: ../images/barcelona_cfw-500_iter.png + :align: center + :width: 590 + :alt: Barcelona Conjugate Frank-Wolfe 500 iterations +| +.. image:: ../images/barcelona_bfw-500_iter.png + :align: center + :width: 590 + :alt: Barcelona Biconjugate Frank-Wolfe 500 iterations + +Chicago Regional +---------------- +Network has: + +* Links: 39,018 +* Nodes: 12,982 +* Zones: 1,790 + +.. image:: ../images/chicago_regional_msa-500_iter.png + :align: center + :width: 590 + :alt: Chicago MSA 500 iterations +| +.. image:: ../images/chicago_regional_frank-wolfe-500_iter.png + :align: center + :width: 590 + :alt: Chicago Frank-Wolfe 500 iterations +| +.. image:: ../images/chicago_regional_cfw-500_iter.png + :align: center + :width: 590 + :alt: Chicago Conjugate Frank-Wolfe 500 iterations +| +.. image:: ../images/chicago_regional_bfw-500_iter.png + :align: center + :width: 590 + :alt: Chicago Biconjugate Frank-Wolfe 500 iterations + +Convergence Study +----------------- + +Besides validating the final results from the algorithms, we have also compared +how well they converge for the largest instance we have tested (Chicago +Regional), as that instance has a comparable size to real-world models. + +.. image:: ../images/convergence_comparison.png + :align: center + :width: 590 + :alt: Algorithm convergence comparison +| + +Not surprinsingly, one can see that Frank-Wolfe far outperforms the Method of +Successive Averages for a number of iterations larger than 25, and is capable of +reaching 1.0e-04 just after 800 iterations, while MSA is still at 3.5e-4 even +after 1,000 iterations. + +The actual show, however, is left for the Biconjugate Frank-Wolfe +implementation, which delivers a relative gap of under 1.0e-04 in under 200 +iterations, and a relative gap of under 1.0e-05 in just over 700 iterations. + +This convergence capability, allied to its computational performance described +below suggest that AequilibraE is ready to be used in large real-world +applications. + +Computational performance +------------------------- +Running on a IdeaPad laptop equipped with a 6 cores (12 threads) Intel Core i7-10750H +CPU @ 2.60 GHz, and 32GB of RAM, AequilibraE performed 1,000 iterations of +Frank-Wolfe assignment on the Chicago Network in just under 18 minutes, +while Bi-conjugate Frank Wolfe takes just under 19 minutes, or a little more than +1s per All-or-Nothing iteration. + +Compared with AequilibraE previous versions, we can notice a reasonable decrease +in processing time. + +Noteworthy items +---------------- + +.. note:: + The biggest opportunity for performance in AequilibraE right now it to apply + network contraction hierarchies to the building of the graph, but that is + still a long-term goal + +Want to run your own convergence study? +--------------------------------------- + +If you want to run the convergence study in your machine, with Chicago Regional instance +or any other instance presented here, check out the code block below! Please make sure +you have already imported `TNTP files `_ +into your machine. + +In the first part of the code, we'll parse TNTP instances to a format AequilibraE can +understand, and then we'll perform the assignment. + +.. _code-block-for-convergence-study: +.. code-block:: python + + # Imports + import os + import numpy as np + import pandas as pd + from aequilibrae.matrix import AequilibraeMatrix, AequilibraeData + + from aequilibrae.paths import TrafficAssignment + from aequilibrae.paths.traffic_class import TrafficClass + import statsmodels.api as sm + + from aequilibrae.paths import Graph + from copy import deepcopy + + # Folders + data_folder = 'C:/your/path/to/TransportationNetworks/chicago-regional' + matfile = os.path.join(data_folder, 'ChicagoRegional_trips.tntp') + + # Creating the matrix + f = open(matfile, 'r') + all_rows = f.read() + blocks = all_rows.split('Origin')[1:] + matrix = {} + for k in range(len(blocks)): + orig = blocks[k].split('\n') + dests = orig[1:] + orig=int(orig[0]) + + d = [eval('{'+a.replace(';',',').replace(' ','') +'}') for a in dests] + destinations = {} + for i in d: + destinations = {**destinations, **i} + matrix[orig] = destinations + zones = max(matrix.keys()) + index = np.arange(zones) + 1 + mat = np.zeros((zones, zones)) + for i in range(zones): + for j in range(zones): + mat[i, j] = matrix[i+1].get(j+1,0) + + # Let's save our matrix in AequilibraE Matrix format + aemfile = os.path.join(folder, "demand.aem") + aem = AequilibraeMatrix() + kwargs = {'file_name': aem_file, + 'zones': zones, + 'matrix_names': ['matrix'], + "memory_only": False} # in case you want to save the matrix in your machine + + aem.create_empty(**kwargs) + aem.matrix['matrix'][:,:] = mtx[:,:] + aem.index[:] = index[:] + + # Now let's parse the network + net = os.path.join(data_folder, 'ChicagoRegional_net.tntp') + net = pd.read_csv(net, skiprows=7, sep='\t') + + network = net[['init_node', 'term_node', 'free_flow_time', 'capacity', "b", "power"]] + network.columns = ['a_node', 'b_node', 'free_flow_time', 'capacity', "b", "power"] + network = network.assign(direction=1) + network["link_id"] = network.index + 1 + + # If you want to create an AequilibraE matrix for computation, then it follows + g = Graph() + g.cost = net['free_flow_time'].values + g.capacity = net['capacity'].values + g.free_flow_time = net['free_flow_time'].values + + g.network = network + g.network.loc[(g.network.power < 1), "power"] = 1 + g.network.loc[(g.network.free_flow_time == 0), "free_flow_time"] = 0.01 + g.network_ok = True + g.status = 'OK' + g.prepare_graph(index) + g.set_graph("free_flow_time") + g.set_skimming(["free_flow_time"]) + g.set_blocked_centroid_flows(True) + + # We run the traffic assignment + for algorithm in ["bfw", "fw", "cfw", "msa"]: + + mat = AequilibraeMatrix() + mat.load(os.path.join(data_folder, "demand.aem")) + mat.computational_view(["matrix"]) + + assigclass = TrafficClass("car", g, mat) + + assig = TrafficAssignment() + + assig.set_classes([assigclass]) + assig.set_vdf("BPR") + assig.set_vdf_parameters({"alpha": "b", "beta": "power"}) + assig.set_capacity_field("capacity") + assig.set_time_field("free_flow_time") + assig.max_iter = 1000 + assig.rgap_target = 1e-10 + assig.set_algorithm(algorithm) + + assig.execute() + assigclass.results.save_to_disk( + os.path.join(data_folder, f"convergence_study/results-1000.aed")) + + assig.report().to_csv(os.path.join(data_folder, f"{algorithm}_computational_results.csv")) + +As we've exported the assignment's results into CSV files, we can use Pandas to read the files, +and plot a graph just :ref:`like the one above `. \ No newline at end of file diff --git a/docs/source/version_history.rst b/docs/source/version_history.rst new file mode 100644 index 000000000..960a3f0a1 --- /dev/null +++ b/docs/source/version_history.rst @@ -0,0 +1,103 @@ +.. _versionhistory: + +Older versions +============== + +AequilibraE has been evolving quite fast, so we recommend you upgrading to a +newer version as soon as you can. In the meantime, you can find the +documentation for all versions since 0.5.3. + +.. panels:: + :container: container-lg pb-3 + :column: col-lg-4 col-lg-4 col-lg-4 p-2 + + .. link-button:: https://aequilibrae.com/python/V.0.5.3/ + :text: 0.5.3 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.6.0/ + :text: 0.6.0 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.6.1/ + :text: 0.6.1 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.6.2/ + :text: 0.6.2 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.6.3/ + :text: 0.6.3 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.6.4/ + :text: 0.6.4 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.6.5/ + :text: 0.6.5 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.7.0/ + :text: 0.7.0 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.7.1/ + :text: 0.7.1 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.7.2/ + :text: 0.7.2 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.7.3/ + :text: 0.7.3 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.7.4/ + :text: 0.7.4 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.7.5/ + :text: 0.7.5 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.7.6/ + :text: 0.7.6 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.7.7/ + :text: 0.7.7 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.8.0/ + :text: 0.8.0 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.8.1/ + :text: 0.8.1 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.8.2/ + :text: 0.8.2 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.8.3/ + :text: 0.8.3 + :classes: btn-block btn-secondary stretched-link + --- + .. link-button:: https://aequilibrae.com/python/V.0.9.0/ + :text: 0.9.0 + :classes: btn-block btn-secondary stretched-link + + +* `Develop Branch (upcoming version) `_ +This documentation correspond to software version: + +.. git_commit_detail:: + :branch: + :commit: + :sha_length: 10 + :uncommitted: + :untracked: \ No newline at end of file diff --git a/docs/table_documentation.py b/docs/table_documentation.py new file mode 100644 index 000000000..068a59aaa --- /dev/null +++ b/docs/table_documentation.py @@ -0,0 +1,131 @@ +import shutil +import sys +from os.path import join, realpath +from pathlib import Path +from tempfile import gettempdir +from typing import List +from uuid import uuid4 + +project_dir = Path(__file__).parent.parent +if str(project_dir) not in sys.path: + sys.path.append(str(project_dir)) + + +class CreateTablesSRC: + def __init__(self, component: str, tgt_fldr: str): + from aequilibrae.project import Project + from aequilibrae.project.database_connection import database_connection + from aequilibrae.transit import Transit + + # Create a new project + self.proj_path = join(gettempdir(), f"aequilibrae_{uuid4().hex[:6]}") + self.proj = Project() + self.proj.new(self.proj_path) + Transit(self.proj) + + folder = "network" if component == "project_database" else "transit" + self.stub = "data_model" + # Get the appropriate data for the database we are documenting + self.conn = database_connection(db_type=folder, project_path=self.proj_path) + self.path = join(*Path(realpath(__file__)).parts[:-1], + f"../aequilibrae/project/database_specification/{folder}/tables") + self.doc_path = str(Path(realpath(__file__)).parent / "source" / tgt_fldr) + + Path(join(self.doc_path, self.stub)).mkdir(exist_ok=True, parents=True) + + def create(self): + datamodel_rst = join(self.doc_path, self.stub, "datamodel.rst") + shutil.copyfile(join(self.doc_path, "datamodel.rst.template"), datamodel_rst) + placeholder = "LIST_OF_TABLES" + tables_txt = "table_list.txt" + all_tables = [e for e in self.readlines(join(self.path, tables_txt)) if e != "migrations"] + + for table_name in all_tables: + descr = self.conn.execute(f"pragma table_info({table_name})").fetchall() + + # Title of the page + title = f'**{table_name.replace("_", " ")}** table structure' + txt = [title, "=" * len(title), ""] + + docstrings = self.__get_docstrings(table_name) + sql_code = self.__get_sql_code(table_name) + + txt.extend(docstrings) + + txt.append("") + txt.append(".. csv-table:: ") + txt.append(' :header: "Field", "Type", "NULL allowed", "Default Value"') + txt.append(" :widths: 30, 20, 20, 20") + txt.append("") + + for dt in descr: + data = list(dt[1:-1]) + if dt[-1] == 1: + data[0] += "*" + + if data[-1] is None: + data[-1] = "" + + if data[2] == 1: + data[2] = "NO" + else: + data[2] = "YES" + + txt.append(" " + ",".join([str(x) for x in data])) + txt.append("\n\n(* - Primary key)") + + txt.extend(sql_code) + + output = join(self.doc_path, self.stub, f"{table_name}.rst") + with open(output, "w") as f: + for line in txt: + f.write(line + "\n") + + all_tables = [f" {x.rstrip()}.rst\n" for x in sorted(all_tables, reverse=True)] + + with open(datamodel_rst, "r") as lst: + datamodel = lst.readlines() + + for i, line in enumerate(datamodel): + if placeholder in line: + datamodel[i] = "" + for tb in all_tables: + datamodel.insert(i, tb) + break + + with open(datamodel_rst, "w") as lst: + for line in datamodel: + lst.write(line) + + def __get_docstrings(self, table_name: str) -> List[str]: + with open(join(self.path, table_name + ".sql"), "r") as f: + lines = f.readlines() + + docstring = [] + for line in lines: + if "--@" == line[:3]: + text = line[3:].rstrip() + docstring.append(text.strip().rstrip().lstrip()) + return docstring + + def __get_sql_code(self, table_name: str) -> List[str]: + with open(join(self.path, table_name + ".sql"), "r") as f: + lines = f.readlines() + + sql_code = ["\n\n", "The SQL statement for table and index creation is below.\n\n", "::\n"] + for line in lines: + if "--" not in line: + sql_code.append(f" {line.rstrip()}") + return sql_code + + @staticmethod + def readlines(filename): + with open(filename, "r") as f: + return [x.strip() for x in f.readlines()] + +tables = [("project_database", "modeling_with_aequilibrae/project_database"), + ("transit_database", "modeling_with_aequilibrae/transit_database")] + +for table, pth in tables: + s = CreateTablesSRC(table, pth) + s.create() diff --git a/docs/website/check_documentation_versions.py b/docs/website/check_documentation_versions.py index d59e9627e..a83fb9c6c 100644 --- a/docs/website/check_documentation_versions.py +++ b/docs/website/check_documentation_versions.py @@ -1,17 +1,19 @@ import os import sys -import shutil +from pathlib import Path -npth = os.path.abspath(".") +npth = Path(__file__).parent.parent.parent if npth not in sys.path: sys.path.append(npth) + print(npth) -from __version__ import release_version +with open(npth / "__version__.py") as f: + exec(f.read()) # We check if the reference to all existing versions were added by checking # that the current version is referenced -with open(os.path.join(npth, "docs/source/index.rst"), mode="r") as f: +with open(os.path.join(npth, "docs/source/version_history.rst"), mode="r") as f: txt = f.read() -assert f"`{release_version}" in txt +assert f"{release_version}" in txt assert f"V.{release_version}" in txt diff --git a/requirements.txt b/requirements.txt index e0ff866f2..7c3dd56c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ numpy scipy pyaml cython -pyshp requests shapely pandas