Skip to content

Commit

Permalink
Merge branch 'develop' into route_choice
Browse files Browse the repository at this point in the history
  • Loading branch information
Jake-Moss committed Jan 31, 2024
2 parents 4b65f66 + 6dc3427 commit f0b5cbd
Show file tree
Hide file tree
Showing 64 changed files with 507 additions and 408 deletions.
File renamed without changes.
32 changes: 16 additions & 16 deletions .github/workflows/build_linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,22 @@ jobs:
mkdir -p dist
cp -v ./*-manylinux*.whl dist/
- name: update versions to conform with QGIS
run: |
cd .github
python qgis_requirements.py
cd ..
- name: Build manylinux Python wheels
uses: RalfG/[email protected]
with:
python-versions: 'cp39-cp39'
pip-wheel-args: '--no-deps'

- name: Moves wheels
run: |
mkdir -p dist
cp -v ./*-manylinux*.whl dist/
# - name: update versions to conform with QGIS
# run: |
# cd .github
# python qgis_requirements.py
# cd ..
#
# - name: Build manylinux Python wheels
# uses: RalfG/[email protected]
# with:
# python-versions: 'cp39-cp39'
# pip-wheel-args: '--no-deps'
#
# - name: Moves wheels
# run: |
# mkdir -p dist
# cp -v ./*-manylinux*.whl dist/

- name: Stores artifacts along with the workflow result
if: ${{ github.event_name == 'push'}}
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ jobs:
pip install -r requirements.txt
pip install -r requirements_additional.txt
pip install -r docs/requirements-docs.txt
python -m pip install sphinx-gallery --user
sudo apt update
sudo apt install -y --fix-missing libsqlite3-mod-spatialite libspatialite-dev pandoc
sudo ln -s /usr/lib/x86_64-linux-gnu/mod_spatialite.so /usr/lib/x86_64-linux-gnu/mod_spatialite
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ jobs:
pip install -r requirements_additional.txt
pip install -r tests/requirements_tests.txt
- name: Lint with flake8
run: flake8
- name: Lint with ruff
run: ruff .

- name: Check code format with Black
run: black --check .
Expand Down
56 changes: 53 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,59 @@
[![QAequilibraE artifacts](https://github.com/AequilibraE/aequilibrae/actions/workflows/build_artifacts_qgis.yml/badge.svg)](https://github.com/AequilibraE/aequilibrae/actions/workflows/build_artifacts_qgis.yml)


AequilibraE is a fully-featured Open-Source transportation modeling package and the first comprehensive package
of its kind for the Python ecosystem. It aims to provide all the fundamental transport modelling
resources not available from other open-source packages in the Python (NumPy, really) ecosystem.
AequilibraE is a fully-featured Open-Source transportation modeling package and
the first comprehensive package of its kind for the Python ecosystem, and is
released under an extremely permissive and business-friendly license.

It is developed as general-purpose modeling software and imposes very little
underlying structure on models built upon it. This flexibility also extends to
the ability of using all its core algorithms without an actual AequilibraE
model by simply building very simple memory objects from Pandas DataFrames, and
NumPY arrays, making it the perfect candidate for use-cases where transport is
one component of a bigger and more general planning or otherwise analytical
modeling pipeline.

Different than in traditional packages, AequilibraE's network is stored in
SQLite/Spatialite, a widely supported open format, and its editing capabilities
are built into its data layer through a series of spatial database triggers,
which allows network editing to be done on Any GIS package supporting SpatiaLite,
through a dedicated Python API or directly from an SQL console while maintaining
full geographical consistency between links and nodes, as well as data integrity
and consistency with other model tables.

AequilibraE provides full support for OMX matrices, which can be used as input
for any AequilibraE procedure, and makes its outputs, particularly skim matrices
readily available to other modeling activities.

AequilibraE includes multi-class user-equilibrium assignment with full support
for class-specific networks, value-of-time and generalized cost functions, and
includes a range of equilibration algorithms, including MSA, the traditional
Frank-Wolfe as well as the state-of-the-art Bi-conjugate Frank-Wolfe.

AequilibraE's support for public transport includes a GTFS importer that can
map-match routes into the model network and an optimized version of the
traditional "Optimal-Strategies" transit assignment, and full support in the data
model for other schedule-based assignments to be implemented in the future.

State-of-the-art computational performance and full multi-threading can be
expected from all key algorithms in AequilibraE, from cache-optimized IPF,
to path-computation based on sophisticated data structures and cascading network
loading, which all ensure that AequilibraE performs at par with the best
commercial packages current available on the market.

AequilibraE has also a Graphical Interface for the popular GIS package QGIS,
which gives access to most AequilibraE procedures and includes a wide range of
visualization tools, such as flow maps, desire and delaunay lines, scenario
comparison, matrix visualization, etc. This GUI, called QAequilibraE, is
currently available in English, French and Portuguese and more languages are
continuously being added, which is another substantial point of difference from
commercial packages.

Finally, AequilibraE is developed 100% in the open and incorporates software-development
best practices for testing and documentation. AequilibraE's testing includes all
major operating systems (Windows, Linux and MacOS) and all currently supported versions
of Python. AequilibraE is also supported on ARM-based cloud computation nodes, making
cloud deployments substantially less expensive.

## Comprehensive documentation

Expand Down
1 change: 1 addition & 0 deletions aequilibrae/distribution/gravity_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
The procedures implemented in this code are some of those suggested in
Modelling Transport, 4th Edition, Ortuzar and Willumsen, Wiley 2011
"""

from time import perf_counter

import numpy as np
Expand Down
2 changes: 1 addition & 1 deletion aequilibrae/distribution/synthetic_gravity_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def load(self, file_name):
else:
raise ValueError("Model has unknown parameters: " + str(key))
except ValueError as err:
raise ValueError("File provided is not a valid Synthetic Gravity Model - {}".format(err.__str__()))
raise ValueError("File provided is not a valid Synthetic Gravity Model - {}".format(err.__str__())) from err

def save(self, file_name):
R"""Saves model to disk in yaml format. Extension is \*.mod"""
Expand Down
11 changes: 3 additions & 8 deletions aequilibrae/matrix/aequilibrae_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,18 @@ def create_empty(

if not isinstance(self.data_types, list):
raise ValueError('Data types, "data_types", needs to be a list')
# The check below is not working properly with the QGIS importer
# else:
# for dt in self.data_types:
# if not isinstance(dt, type):
# raise ValueError('Data types need to be Python or Numpy data types')

for field in self.fields:
if not type(field) is str:
raise TypeError(field + " is not a string. You cannot use it as a field name")
if not isinstance(field, str):
raise TypeError(f"{field} is not a string. You cannot use it as a field name")
if not field.isidentifier():
raise Exception(field + " is a not a valid identifier name. You cannot use it as a field name")
if field in object.__dict__:
raise Exception(field + " is a reserved name. You cannot use it as a field name")

self.num_fields = len(self.fields)

dtype = [("index", self.aeq_index_type)] + [(f, dt) for f, dt in zip(self.fields, self.data_types)]
dtype = [("index", self.aeq_index_type)] + list(zip(self.fields, self.data_types))

# the file
if self.memory_mode:
Expand Down
60 changes: 49 additions & 11 deletions aequilibrae/matrix/aequilibrae_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import tempfile
import uuid
import warnings
from copy import copy
from functools import reduce
from pathlib import Path
from typing import List

import numpy as np
Expand Down Expand Up @@ -90,14 +92,18 @@ def __init__(self):
self.omx_file = None # type: omx.File
self.__version__ = VERSION # Writes file version

def save(self, names=()) -> None:
def save(self, names=(), file_name=None) -> None:
"""Saves matrix data back to file.
If working with AEM file, it flushes data to disk. If working with OMX, requires new names.
:Arguments:
**names** (:obj:`tuple(str)`, `Optional`): New names for the matrices. Required if working with OMX files
"""
if file_name is not None:
cores = names if len(names) else self.names
self.__save_as(file_name, cores)
return

if not self.__omx:
self.__flush(self.matrices)
Expand All @@ -122,6 +128,38 @@ def save(self, names=()) -> None:
self.names = self.omx_file.list_matrices()
self.computational_view(names)

def __save_as(self, file_name: str, cores: List[str]):

if Path(file_name).suffix.lower() == ".aem":
mat = AequilibraeMatrix()
args = {
"zones": self.zones,
"matrix_names": cores,
"index_names": self.index_names,
"memory_only": False,
"file_name": file_name,
}
mat.create_empty(**args)
mat.indices[:, :] = self.indices[:, :]
for core in cores:
mat.matrix[core][:, :] = self.matrix[core][:, :]
mat.name = self.name
mat.description = self.description
mat.close()
del mat

elif Path(file_name).suffix.lower() == ".omx":
omx_mat = omx.open_file(file_name, "w")
for core in cores:
omx_mat[core] = self.matrix[core]

for index in self.index_names:
omx_mat.create_mapping(index, self.indices[index])

omx_mat.attrs.name = self.name
omx_mat.attrs.description = self.description
omx_mat.close()

def create_empty(
self,
file_name: str = None,
Expand Down Expand Up @@ -221,7 +259,7 @@ def create_empty(
else:
raise Exception("Matrix names need to be provided as a list")

self.names = [x for x in matrix_names]
self.names = copy(matrix_names)
self.cores = len(self.names)
if self.zones is None:
return
Expand Down Expand Up @@ -344,8 +382,8 @@ def robust_name(input_name: str, max_length: int, forbiden_names: List[str]) ->
)
idx_names = functools.reduce(lambda acc, n: acc + [robust_name(n, INDEX_NAME_MAX_LENGTH, acc)], do_idx, [])
else:
core_names = [x for x in do_cores]
idx_names = [x for x in do_idx]
core_names = list(do_cores)
idx_names = list(do_idx)

self.create_empty(
file_name=file_path,
Expand Down Expand Up @@ -391,7 +429,7 @@ def create_from_trip_list(self, path_to_file: str, from_column: str, to_column:
trip_df = pd.read_csv(path_to_file)

# Creating zone indices
zones_list = sorted(list(set(list(trip_df[from_column].unique()) + list(trip_df[to_column].unique()))))
zones_list = sorted(set(list(trip_df[from_column].unique()) + list(trip_df[to_column].unique())))
zones_df = pd.DataFrame({"zone": zones_list, "idx": list(np.arange(len(zones_list)))})

trip_df = trip_df.merge(
Expand Down Expand Up @@ -570,9 +608,9 @@ def __write__(self):
np.memmap(self.file_path, dtype="uint8", offset=17, mode="r+", shape=1)[0] = data_size

# matrix name
np.memmap(self.file_path, dtype="S" + str(MATRIX_NAME_MAX_LENGTH), offset=18, mode="r+", shape=1)[
0
] = self.name
np.memmap(self.file_path, dtype="S" + str(MATRIX_NAME_MAX_LENGTH), offset=18, mode="r+", shape=1)[0] = (
self.name
)

# matrix description
offset = 18 + MATRIX_NAME_MAX_LENGTH
Expand Down Expand Up @@ -1095,9 +1133,9 @@ def setName(self, matrix_name: str):
if len(str(matrix_name)) > MATRIX_NAME_MAX_LENGTH:
matrix_name = str(matrix_name)[0:MATRIX_NAME_MAX_LENGTH]

np.memmap(self.file_path, dtype="S" + str(MATRIX_NAME_MAX_LENGTH), offset=18, mode="r+", shape=1)[
0
] = matrix_name
np.memmap(self.file_path, dtype="S" + str(MATRIX_NAME_MAX_LENGTH), offset=18, mode="r+", shape=1)[0] = (
matrix_name
)

def setDescription(self, matrix_description: str):
"""
Expand Down
13 changes: 5 additions & 8 deletions aequilibrae/paths/assignment_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,12 @@ def _read_compressed_graph_correspondence(self) -> Dict:

def read_path_file(self, origin: int, iteration: int, traffic_class_id: str) -> (pd.DataFrame, pd.DataFrame):
possible_traffic_classes = list(filter(lambda x: x.id == traffic_class_id, self.classes))
assert (
len(possible_traffic_classes) == 1
), f"traffic class id not unique, please choose one of {list(map(lambda x: x.id, self.classes))}"
class_ids = [x.id for x in self.classes]
assert len(possible_traffic_classes) == 1, f"traffic class id not unique, please choose one of {class_ids}"
traffic_class = possible_traffic_classes[0]
base_dir = os.path.join(
self.path_base_dir, f"iter{iteration}", f"path_c{traffic_class.id}_{traffic_class.name}"
)
path_o_f = os.path.join(base_dir, f"o{origin}.feather")
path_o_index_f = os.path.join(base_dir, f"o{origin}_indexdata.feather")
b_dir = os.path.join(self.path_base_dir, f"iter{iteration}", f"path_c{traffic_class.id}_{traffic_class.name}")
path_o_f = os.path.join(b_dir, f"o{origin}.feather")
path_o_index_f = os.path.join(b_dir, f"o{origin}_indexdata.feather")
path_o = pd.read_feather(path_o_f)
path_o_index = pd.read_feather(path_o_index_f)
return path_o, path_o_index
Expand Down
16 changes: 7 additions & 9 deletions aequilibrae/paths/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from aequilibrae.context import get_logger


class GraphBase(ABC):
class GraphBase(ABC): # noqa: B024
"""
Graph class
"""
Expand Down Expand Up @@ -244,8 +244,8 @@ def exclude_links(self, links: list) -> None:
self._id = uuid.uuid4().hex

def __build_column_names(self, all_titles: List[str]) -> Tuple[list, list]:
fields = [x for x in self.required_default_fields]
types = [x for x in self.__required_default_types]
fields = list(self.required_default_fields)
types = list(self.__required_default_types)
for column in all_titles:
if column not in self.required_default_fields and column[0:-3] not in self.required_default_fields:
if column[-3:] == "_ab":
Expand Down Expand Up @@ -462,19 +462,17 @@ def __determine_types__(self, new_type, current_type):
new_type = float(new_type)
except ValueError as verr:
self.logger.warning("Could not convert {} - {}".format(new_type, verr.__str__()))
nt = type(new_type)
def_type = None
if nt == int:
if isinstance(new_type, int):
def_type = int
if current_type == float:
def_type == float
def_type = float
elif current_type == str:
def_type = str
elif nt == float:
elif isinstance(new_type, float):
def_type = float
if current_type == str:
def_type = str
elif nt == str:
elif isinstance(new_type, str):
def_type = str
else:
raise ValueError("WRONG TYPE OR NULL VALUE")
Expand Down
5 changes: 2 additions & 3 deletions aequilibrae/paths/linear_approximation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from aequilibrae.paths.all_or_nothing import allOrNothing
from aequilibrae.paths.results import AssignmentResults
from aequilibrae.paths.traffic_class import TrafficClass
from aequilibrae.context import get_active_project
from ..utils import WorkerThread

try:
Expand Down Expand Up @@ -342,7 +341,7 @@ def __maybe_create_path_file_directories(self):
def doWork(self):
self.execute()

def execute(self):
def execute(self): # noqa: C901
# We build the fixed cost field

for c in self.traffic_classes:
Expand Down Expand Up @@ -371,7 +370,7 @@ def execute(self):

self.logger.info(f"{self.algorithm} Assignment STATS")
self.logger.info("Iteration, RelativeGap, stepsize")
for self.iter in range(1, self.max_iter + 1):
for self.iter in range(1, self.max_iter + 1): # noqa: B020
self.iteration_issue = []
if pyqt:
self.equilibration.emit(["rgap", self.rgap])
Expand Down
1 change: 1 addition & 0 deletions aequilibrae/paths/results/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
STILL NEED TO ADD SOME EXPLANATIONS HERE
"""

__author__ = "Pedro Camargo ($Author: Pedro Camargo $)"
__version__ = "1.0"
__revision__ = "$Revision: 1 $"
Expand Down
Loading

0 comments on commit f0b5cbd

Please sign in to comment.