Skip to content

Commit

Permalink
Release 0.9.3 (#432)
Browse files Browse the repository at this point in the history
* fixes errors on qgis (#417)

* runs examples (#422)

Co-authored-by: pveigadecamargo <pveigadecamargo@anl.gov>

* Update LICENSE.TXT (#427)

* fix matrix indices for memory-only matrices (#425)

* fix matrix indices for memory-only matrices

* run black

---------

Co-authored-by: Pelle Koster <pelle.koster@nginfra.nl>

* Pedro/cores skimming (#426)

* Moves the setting of number of cores to the right place

* Moves documentation to the right place

---------

Co-authored-by: pveigadecamargo <pveigadecamargo@anl.gov>

* Updates assignment logging (#423)

* assignment logging

* Updates logging

* Adds __config log

* Adds test and docs

* Update aequilibrae/paths/traffic_assignment.py

* Fixes tests

* fixes coverage testing

* merges testing changes

* style

* style

* style

* style

* narrows coverage test

* Update test_traffic_assignment.py

* Apply suggestions from code review

* Fixes test_traffic_assignment

---------

Co-authored-by: pveigadecamargo <pveigadecamargo@anl.gov>
Co-authored-by: Pedro Camargo <c@margo.co>

* narrows coverage test (#429)

Co-authored-by: pveigadecamargo <pveigadecamargo@anl.gov>

* moves openmatrix to a primary dependency (#431)

* moves openmatrix to a primary dependency

* QGIS moved to Numpy 1.24 before moving to Python 3.10

---------

Co-authored-by: pveigadecamargo <pveigadecamargo@anl.gov>

* Updates software version for release

* Bumps up version

* Bumps up documentation version

---------

Co-authored-by: pveigadecamargo <pveigadecamargo@anl.gov>

---------

Co-authored-by: Renata Imai <53949163+r-akemii@users.noreply.github.com>
Co-authored-by: pveigadecamargo <pveigadecamargo@anl.gov>
Co-authored-by: PelleK <elfjes@users.noreply.github.com>
Co-authored-by: Pelle Koster <pelle.koster@nginfra.nl>
  • Loading branch information
5 people committed Jul 10, 2023
1 parent 2139d8b commit fe3f307
Show file tree
Hide file tree
Showing 22 changed files with 507 additions and 271 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[report]
fail_under = 81.0
show_missing = True
2 changes: 1 addition & 1 deletion .github/workflows/build_artifacts_qgis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
continue-on-error: true
strategy:
matrix:
python-version: ['3.9', '3.10']
python-version: ['3.9']
architecture: ['x64']
os: [windows-latest, macos-latest]
steps:
Expand Down
16 changes: 2 additions & 14 deletions .github/workflows/test_linux_with_coverage.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
name: Code coverage

on:
pull_request:
types: [ready_for_review, merge]
on: [pull_request]

jobs:
testing:
Expand Down Expand Up @@ -32,14 +30,4 @@ jobs:
- name: Generate coverage report
run: |
python3 -m pytest --cov=./ --cov-report=xml
- name: Upload coverage to Codecov
if: ${{ (env.HAS_SECRETS == 'true') }}
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
yml: ./codecov.yml
fail_ci_if_error: true
python3 -m pytest --cov=aequilibrae tests/
10 changes: 6 additions & 4 deletions LICENSE.TXT
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
MIT License
MIT License (with added clause)

Copyright (c) 2014-, AequilibraE Developers.

Expand All @@ -12,13 +12,15 @@ furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

Apropriate reference to the software has to be made in all documentation for
work developed with the software, where appropriate.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Additional clause:

Reference to the software has to be made in all documentation for
work developed with the software, where appropriate.
2 changes: 1 addition & 1 deletion __version__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
version = 0.9
minor_version = "2"
minor_version = "3"
release_name = "Queluz"

release_version = f"{version}.{minor_version}"
8 changes: 7 additions & 1 deletion aequilibrae/matrix/aequilibrae_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ def __write__(self):

# Index
if self.__memory_only:
self.indices = np.zeros((self.zones, self.num_indices))
self.indices = np.zeros((self.zones, self.num_indices), dtype=int)
else:
offset += self.num_indices * INDEX_NAME_MAX_LENGTH
self.indices = np.memmap(
Expand Down Expand Up @@ -1081,6 +1081,9 @@ def setName(self, matrix_name: str):
if self.__omx:
raise NotImplementedError("This operation does not make sense for OMX matrices")

if self.__memory_only:
raise NotImplementedError("This operation does not make sense for memory only matrices")

if matrix_name is not None:
if len(str(matrix_name)) > MATRIX_NAME_MAX_LENGTH:
matrix_name = str(matrix_name)[0:MATRIX_NAME_MAX_LENGTH]
Expand Down Expand Up @@ -1114,6 +1117,9 @@ def setDescription(self, matrix_description: str):
if self.__omx:
raise NotImplementedError("This operation does not make sense for OMX matrices")

if self.__memory_only:
raise NotImplementedError("This operation does not make sense for memory only matrices")

if matrix_description is not None:
if len(str(matrix_description)) > MATRIX_DESCRIPTION_MAX_LENGTH:
matrix_description = str(matrix_description)[0:MATRIX_DESCRIPTION_MAX_LENGTH]
Expand Down
39 changes: 34 additions & 5 deletions aequilibrae/paths/network_skimming.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import importlib.util as iutil
import multiprocessing as mp
import sys
import threading
import importlib.util as iutil
from uuid import uuid4
from multiprocessing.dummy import Pool as ThreadPool
from datetime import datetime
from multiprocessing.dummy import Pool as ThreadPool
from uuid import uuid4

from aequilibrae import global_logger
from aequilibrae.context import get_active_project
from aequilibrae.paths.multi_threaded_skimming import MultiThreadedNetworkSkimming
from aequilibrae.paths.results.skim_results import SkimResults
from aequilibrae.utils import WorkerThread
from aequilibrae import global_logger

try:
from aequilibrae.paths.AoN import skimming_single_origin
Expand Down Expand Up @@ -70,6 +72,7 @@ def __init__(self, graph, origins=None, project=None):
self.project = project
self.origins = origins
self.graph = graph
self.cores = mp.cpu_count()
self.results = SkimResults()
self.aux_res = MultiThreadedNetworkSkimming()
self.report = []
Expand All @@ -84,7 +87,7 @@ def execute(self):
"""Runs the skimming process as specified in the graph"""
if pyqt:
self.skimming.emit(["zones finalized", 0])

self.results.cores = self.cores
self.results.prepare(self.graph)
self.aux_res = MultiThreadedNetworkSkimming()
self.aux_res.prepare(self.graph, self.results)
Expand All @@ -109,6 +112,32 @@ def execute(self):
self.skimming.emit(["text skimming", "Saving Outputs"])
self.skimming.emit(["finished_threaded_procedure", None])

def set_cores(self, cores: int) -> None:
"""
Sets number of cores (threads) to be used in computation
Value of zero sets number of threads to all available in the system, while negative values indicate the number
of threads to be left out of the computational effort.
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
:Arguments:
**cores** (:obj:`int`): Number of cores to be used in computation
"""

if isinstance(cores, int):
if cores < 0:
self.cores = max(1, mp.cpu_count() + cores)
if cores == 0:
self.cores = mp.cpu_count()
elif cores > 0:
cores = min(mp.cpu_count(), cores)
if self.cores != cores:
self.cores = cores
else:
raise ValueError("Number of cores needs to be an integer")

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
Expand Down
28 changes: 1 addition & 27 deletions aequilibrae/paths/results/skim_results.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import numpy as np
import multiprocessing as mp

from aequilibrae.matrix.aequilibrae_matrix import AequilibraeMatrix
from aequilibrae.paths.graph import Graph

Expand Down Expand Up @@ -67,29 +67,3 @@ def prepare(self, graph: Graph):
self.skims.matrix_view = self.skims.matrix_view.reshape(self.zones, self.zones, self.num_skims)
self.__graph_id__ = graph.__id__
self.graph = graph

def set_cores(self, cores: int) -> None:
"""
Sets number of cores (threads) to be used in computation
Value of zero sets number of threads to all available in the system, while negative values indicate the number
of threads to be left out of the computational effort.
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
:Arguments:
**cores** (:obj:`int`): Number of cores to be used in computation
"""

if isinstance(cores, int):
if cores < 0:
self.cores = max(1, mp.cpu_count() + cores)
if cores == 0:
self.cores = mp.cpu_count()
elif cores > 0:
cores = min(mp.cpu_count(), cores)
if self.cores != cores:
self.cores = cores
else:
raise ValueError("Number of cores needs to be an integer")
45 changes: 36 additions & 9 deletions aequilibrae/paths/traffic_assignment.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from copy import deepcopy
import importlib.util as iutil
import logging
import socket
import sqlite3
from datetime import datetime
Expand Down Expand Up @@ -102,6 +104,7 @@ def __init__(self, project=None) -> None:
""""""

proj = project or get_active_project(must_exist=False)

par = proj.parameters if proj else Parameters().parameters
parameters = par["assignment"]["equilibrium"]

Expand All @@ -127,6 +130,10 @@ def __init__(self, project=None) -> None:
self.__dict__["steps_below_needed_to_terminate"] = 1
self.__dict__["project"] = proj

self.__dict__["_TrafficAssignment__config"] = {}
self.__dict__["logger"] = None
self.logger = proj.logger if proj else logging.getLogger("aequilibrae")

def __setattr__(self, instance, value) -> None:
check, value, message = self.__check_attributes(instance, value)
if check:
Expand Down Expand Up @@ -195,7 +202,7 @@ def set_classes(self, classes: List[TrafficClass]) -> None:

ids = set([x.__id__ for x in classes])
if len(ids) < len(classes):
raise Exception("Classes need to be unique. Your list of classes has repeated items/IDs")
raise ValueError("Classes need to be unique. Your list of classes has repeated items/IDs")
self.classes = classes # type: List[TrafficClass]

def add_class(self, traffic_class: TrafficClass) -> None:
Expand All @@ -208,7 +215,7 @@ def add_class(self, traffic_class: TrafficClass) -> None:

ids = [x.__id__ for x in self.classes if x.__id__ == traffic_class.__id__]
if len(ids) > 0:
raise Exception("Traffic class already in the assignment")
raise ValueError("Traffic class already in the assignment")

self.classes.append(traffic_class)

Expand Down Expand Up @@ -245,9 +252,12 @@ def set_algorithm(self, algorithm: str):
if algo in ["all-or-nothing", "msa", "frank-wolfe", "cfw", "bfw"]:
self.assignment = LinearApproximation(self, algo, project=self.project)
else:
raise Exception("Algorithm not listed in the case selection")
raise ValueError("Algorithm not listed in the case selection")

self.__dict__["algorithm"] = algo
self.__config["Algorithm"] = algo
self.__config["Maximum iterations"] = self.assignment.max_iter
self.__config["Target RGAP"] = self.assignment.rgap_target

def set_vdf_parameters(self, par: dict) -> None:
"""
Expand All @@ -261,8 +271,11 @@ def set_vdf_parameters(self, par: dict) -> None:
"""
if self.classes is None or self.vdf.function.lower() not in all_vdf_functions:
raise Exception("Before setting vdf parameters, you need to set traffic classes and choose a VDF function")
raise RuntimeError(
"Before setting vdf parameters, you need to set traffic classes and choose a VDF function"
)
self.__dict__["vdf_parameters"] = par
self.__config["VDF parameters"] = par
pars = []
if self.vdf.function in ["BPR", "BPR2", "CONICAL", "INRETS"]:
for p1 in ["alpha", "beta"]:
Expand All @@ -289,6 +302,7 @@ def set_vdf_parameters(self, par: dict) -> None:
raise ValueError(f"At least one {p1} is smaller than one. Results will make no sense")

self.__dict__["vdf_parameters"] = pars
self.__config["VDF function"] = self.vdf.function.lower()

def set_cores(self, cores: int) -> None:
"""Allows one to set the number of cores to be used AFTER traffic classes have been added
Expand All @@ -299,7 +313,7 @@ def set_cores(self, cores: int) -> None:
**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")
raise RuntimeError("You need load traffic classes before overwriting the number of cores")

self.cores = cores
for c in self.classes:
Expand All @@ -313,7 +327,7 @@ def set_save_path_files(self, save_it: bool) -> None:
**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")
raise RuntimeError("You need to set traffic classes before turning path saving on or off")

# self.save_path_files = save_it
for c in self.classes:
Expand All @@ -326,7 +340,7 @@ def set_path_file_format(self, file_format: str) -> None:
**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")
raise RuntimeError("You need to set traffic classes before specifying path saving options")

if file_format == "feather":
for c in self.classes:
Expand All @@ -335,7 +349,7 @@ def set_path_file_format(self, file_format: str) -> None:
for c in self.classes:
c._aon_results.write_feather = False
else:
raise Exception(f"Unsupported path file format {file_format} - only feather or parquet available.")
raise TypeError(f"Unsupported path file format {file_format} - only feather or parquet available.")

def set_time_field(self, time_field: str) -> None:
"""
Expand Down Expand Up @@ -363,6 +377,7 @@ def set_time_field(self, time_field: str) -> None:
self.__dict__["congested_time"] = np.array(self.free_flow_tt, copy=True)
self.__dict__["total_flow"] = np.zeros(self.free_flow_tt.shape[0], np.float64)
self.time_field = time_field
self.__config["Time field"] = time_field

def set_capacity_field(self, capacity_field: str) -> None:
"""
Expand All @@ -389,6 +404,8 @@ def set_capacity_field(self, capacity_field: str) -> None:
self.__dict__["capacity"] = np.zeros(c.graph.graph.shape[0], c.graph.default_types("float"))
self.__dict__["capacity"][c.graph.graph.__supernet_id__] = c.graph.graph[capacity_field]
self.capacity_field = capacity_field
self.__config["Number of cores"] = c.results.cores
self.__config["Capacity field"] = capacity_field

# TODO: This function actually needs to return a human-readable dictionary, and not one with
# tons of classes. Feeds into the class above
Expand All @@ -408,10 +425,20 @@ def __validate_parameters(self, kwargs) -> bool:
raise ValueError("List of functions {} for vdf {} has an inadequate set of parameters".format(q, self.vdf))
return True

def execute(self) -> None:
def execute(self, log_specification=True) -> None:
"""Processes assignment"""
if log_specification:
self.log_specification()
self.assignment.execute()

def log_specification(self):
self.logger.info("Traffic Class specification")
for cls in self.classes:
self.logger.info(str(cls.info))

self.logger.info("Traffic Assignment specification")
self.logger.info(self.__config)

def save_results(self, table_name: str, keep_zero_flows=True, project=None) -> None:
"""Saves the assignment results to results_database.sqlite
Expand Down
Loading

0 comments on commit fe3f307

Please sign in to comment.