From 44525c34e897f912a64107cf50de9fb1beee5fee Mon Sep 17 00:00:00 2001 From: Renata Imai <53949163+r-akemii@users.noreply.github.com> Date: Thu, 25 May 2023 21:24:54 -0300 Subject: [PATCH 1/9] fixes errors on qgis (#417) --- aequilibrae/transit/lib_gtfs.py | 2 +- aequilibrae/utils/geo_index.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/aequilibrae/transit/lib_gtfs.py b/aequilibrae/transit/lib_gtfs.py index 69ef3b473..2e8f4aaf0 100644 --- a/aequilibrae/transit/lib_gtfs.py +++ b/aequilibrae/transit/lib_gtfs.py @@ -218,7 +218,7 @@ def execute_import(self): self.logger.info(f" Importing feed for agency {self.gtfs_data.agency.agency} on {self.day}") self.__mt = f"Importing {self.gtfs_data.agency.agency} to supply" - self.signal.emit(["start", "master", self.day, self.__mt]) + self.signal.emit(["start", "master", 1, self.day, self.__mt]) self.save_to_disk() diff --git a/aequilibrae/utils/geo_index.py b/aequilibrae/utils/geo_index.py index 24a3fd1c5..492e0271b 100644 --- a/aequilibrae/utils/geo_index.py +++ b/aequilibrae/utils/geo_index.py @@ -75,9 +75,17 @@ def nearest(self, geo: Union[Point, Polygon, LineString, MultiPoint, MultiPolygo warnings.warn("You need RTREE to build a spatial index") def delete(self, feature_id, geometry: Union[Point, Polygon, LineString, MultiPoint, MultiPolygon]): - if env not in ["QGIS", "Python"]: + if env == "QGIS": + g = QgsGeometry() + g.fromWkb(geometry.wkb) + feature = QgsFeature() + feature.setGeometry(g) + feature.setId(feature_id) + self.idx.deleteFeature(feature) + elif env == "Python": + self.idx.delete(feature_id, geometry.bounds) + else: warnings.warn("You need RTREE to build a spatial index") - self.idx.delete(feature_id, geometry.bounds) def reset(self): self.idx = Index() From a2a709b4fb4b6504960540eccb975ae9456a87e1 Mon Sep 17 00:00:00 2001 From: Pedro Camargo Date: Mon, 26 Jun 2023 03:34:29 -0700 Subject: [PATCH 2/9] runs examples (#422) Co-authored-by: pveigadecamargo --- docs/source/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 038172e2c..2073dde09 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -74,7 +74,6 @@ "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/editing_networks", "examples/trip_distribution", From b3d4e37244f09439b0d10d4d3aadb583160f0c84 Mon Sep 17 00:00:00 2001 From: Pedro Camargo Date: Thu, 29 Jun 2023 03:24:38 -0700 Subject: [PATCH 3/9] Update LICENSE.TXT (#427) --- LICENSE.TXT | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/LICENSE.TXT b/LICENSE.TXT index 954d29687..1e5685362 100644 --- a/LICENSE.TXT +++ b/LICENSE.TXT @@ -1,4 +1,4 @@ -MIT License +MIT License (with added clause) Copyright (c) 2014-, AequilibraE Developers. @@ -12,9 +12,6 @@ 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 @@ -22,3 +19,8 @@ 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. From 79bf1275c239bfa0356133c68216d9cde13c9e67 Mon Sep 17 00:00:00 2001 From: PelleK Date: Mon, 3 Jul 2023 01:21:22 +0200 Subject: [PATCH 4/9] fix matrix indices for memory-only matrices (#425) * fix matrix indices for memory-only matrices * run black --------- Co-authored-by: Pelle Koster --- aequilibrae/matrix/aequilibrae_matrix.py | 8 +- aequilibrae/paths/traffic_assignment.py | 18 +- .../paths/test_traffic_assignment.py | 351 ++++++++++-------- 3 files changed, 204 insertions(+), 173 deletions(-) diff --git a/aequilibrae/matrix/aequilibrae_matrix.py b/aequilibrae/matrix/aequilibrae_matrix.py index 11928d9a1..72f79d11b 100644 --- a/aequilibrae/matrix/aequilibrae_matrix.py +++ b/aequilibrae/matrix/aequilibrae_matrix.py @@ -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( @@ -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] @@ -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] diff --git a/aequilibrae/paths/traffic_assignment.py b/aequilibrae/paths/traffic_assignment.py index 65be6fa3b..860ccb86a 100644 --- a/aequilibrae/paths/traffic_assignment.py +++ b/aequilibrae/paths/traffic_assignment.py @@ -195,7 +195,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: @@ -208,7 +208,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) @@ -245,7 +245,7 @@ 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 @@ -261,7 +261,9 @@ 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 pars = [] if self.vdf.function in ["BPR", "BPR2", "CONICAL", "INRETS"]: @@ -299,7 +301,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: @@ -313,7 +315,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: @@ -326,7 +328,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: @@ -335,7 +337,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: """ diff --git a/tests/aequilibrae/paths/test_traffic_assignment.py b/tests/aequilibrae/paths/test_traffic_assignment.py index 554e841a9..f3f473612 100644 --- a/tests/aequilibrae/paths/test_traffic_assignment.py +++ b/tests/aequilibrae/paths/test_traffic_assignment.py @@ -2,223 +2,274 @@ import random import sqlite3 import string -import uuid from random import choice -from tempfile import gettempdir -from unittest import TestCase import numpy as np import pandas as pd +import pytest from aequilibrae import TrafficAssignment, TrafficClass, Graph +from aequilibrae.project.project import Project from aequilibrae.utils.create_example import create_example from ...data import siouxfalls_project -class TestTrafficAssignment(TestCase): - def setUp(self) -> None: - os.environ["PATH"] = os.path.join(gettempdir(), "temp_data") + ";" + os.environ["PATH"] +@pytest.fixture +def project(tmp_path): + proj = create_example(str(tmp_path / "test_traffic_assignment")) + proj.network.build_graphs() + proj.activate() + return proj - proj_path = os.path.join(gettempdir(), "test_traffic_assignment_" + uuid.uuid4().hex) - self.project = create_example(proj_path) - self.project.network.build_graphs() - self.car_graph = self.project.network.graphs["c"] # type: Graph - self.car_graph.set_graph("free_flow_time") - self.car_graph.set_blocked_centroid_flows(False) - self.matrix = self.project.matrices.get_matrix("demand_omx") - self.matrix.computational_view() +@pytest.fixture +def car_graph(project): + graph: Graph = project.network.graphs["c"] + graph.set_graph("free_flow_time") + graph.set_blocked_centroid_flows(False) + return graph - self.assignment = TrafficAssignment() - self.assigclass = TrafficClass("car", self.car_graph, self.matrix) - self.algorithms = ["msa", "cfw", "bfw", "frank-wolfe"] +@pytest.fixture +def matrix(project): + mat = project.matrices.get_matrix("demand_omx") + mat.computational_view() + return mat - def test_matrix_with_wrong_type(self): - self.matrix.matrix_view = np.array(self.matrix.matrix_view, np.int32) - with self.assertRaises(TypeError): - _ = TrafficClass("car", self.car_graph, self.matrix) - def test_set_vdf(self): - with self.assertRaises(ValueError): - self.assignment.set_vdf("CQS") +@pytest.fixture +def assigclass(car_graph, matrix): + return TrafficClass("car", car_graph, matrix) - self.assignment.set_vdf("BPR") - def test_set_classes(self): - with self.assertRaises(AttributeError): - self.assignment.set_classes([1, 2]) +@pytest.fixture +def assignment(project): + return TrafficAssignment(project) - with self.assertRaises(Exception): - self.assignment.set_classes(self.assigclass) - self.assignment.set_classes([self.assigclass]) - # self.fail() +class TestTrafficAssignmentSetup: + algorithms = ["msa", "cfw", "bfw", "frank-wolfe"] - def test_algorithms_available(self): - algs = self.assignment.algorithms_available() + def test_matrix_with_wrong_type(self, matrix, car_graph): + matrix.matrix_view = np.array(matrix.matrix_view, np.int32) + with pytest.raises(TypeError): + TrafficClass("car", car_graph, matrix) + + def test_set_vdf(self, assignment: TrafficAssignment): + with pytest.raises(ValueError): + assignment.set_vdf("CQS") + assignment.set_vdf("BPR") + + def test_set_classes(self, assignment: TrafficAssignment, assigclass: TrafficClass): + with pytest.raises(AttributeError): + assignment.set_classes([1, 2]) + + with pytest.raises(TypeError): + assignment.set_classes(assigclass) + + assignment.set_classes([assigclass]) + + def test_algorithms_available(self, assignment: TrafficAssignment): + algs = assignment.algorithms_available() real = ["all-or-nothing", "msa", "frank-wolfe", "bfw", "cfw", "fw"] diff = [x for x in real if x not in algs] diff2 = [x for x in algs if x not in real] - if len(diff) + len(diff2) > 0: - self.fail("list of algorithms raised is wrong") + assert len(diff) + len(diff2) <= 0, "list of algorithms raised is wrong" - def test_set_cores(self): - with self.assertRaises(Exception): - self.assignment.set_cores(3) + def test_set_cores(self, assignment: TrafficAssignment, assigclass: TrafficClass): + with pytest.raises(Exception): + assignment.set_cores(3) - self.assignment.add_class(self.assigclass) - with self.assertRaises(ValueError): - self.assignment.set_cores("q") + assignment.add_class(assigclass) + with pytest.raises(ValueError): + assignment.set_cores("q") - self.assignment.set_cores(3) + assignment.set_cores(3) - def test_set_algorithm(self): - with self.assertRaises(AttributeError): - self.assignment.set_algorithm("not an algo") + def test_set_algorithm(self, assignment: TrafficAssignment, assigclass: TrafficClass): + with pytest.raises(AttributeError): + assignment.set_algorithm("not an algo") - self.assignment.add_class(self.assigclass) + assignment.add_class(assigclass) - with self.assertRaises(Exception): - self.assignment.set_algorithm("msa") + with pytest.raises(Exception): + assignment.set_algorithm("msa") - self.assignment.set_vdf("BPR") - self.assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) + assignment.set_vdf("BPR") + assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) - self.assignment.set_capacity_field("capacity") - self.assignment.set_time_field("free_flow_time") + assignment.set_capacity_field("capacity") + assignment.set_time_field("free_flow_time") - self.assignment.max_iter = 10 + assignment.max_iter = 10 for algo in self.algorithms: for _ in range(10): algo = "".join([x.upper() if random.random() < 0.5 else x.lower() for x in algo]) - self.assignment.set_algorithm(algo) + assignment.set_algorithm(algo) - with self.assertRaises(AttributeError): - self.assignment.set_algorithm("not a valid algorithm") + with pytest.raises(AttributeError): + assignment.set_algorithm("not a valid algorithm") - def test_set_vdf_parameters(self): - with self.assertRaises(Exception): - self.assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) + def test_set_vdf_parameters(self, assignment: TrafficAssignment, assigclass: TrafficClass): + with pytest.raises(RuntimeError): + assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) - self.assignment.set_vdf("bpr") - self.assignment.add_class(self.assigclass) - self.assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) + assignment.set_vdf("bpr") + assignment.add_class(assigclass) + assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) - def test_set_time_field(self): - with self.assertRaises(ValueError): - self.assignment.set_time_field("capacity") + def test_set_time_field(self, assignment: TrafficAssignment, assigclass: TrafficClass): + with pytest.raises(ValueError): + assignment.set_time_field("capacity") - self.assignment.add_class(self.assigclass) + assignment.add_class(assigclass) N = random.randint(1, 50) val = "".join(random.choices(string.ascii_uppercase + string.digits, k=N)) - with self.assertRaises(ValueError): - self.assignment.set_time_field(val) + with pytest.raises(ValueError): + assignment.set_time_field(val) - self.assignment.set_time_field("free_flow_time") - self.assertEqual(self.assignment.time_field, "free_flow_time") + assignment.set_time_field("free_flow_time") + assert assignment.time_field == "free_flow_time" - def test_set_capacity_field(self): - with self.assertRaises(ValueError): - self.assignment.set_capacity_field("capacity") + def test_set_capacity_field(self, assignment: TrafficAssignment, assigclass: TrafficClass): + with pytest.raises(ValueError): + assignment.set_capacity_field("capacity") - self.assignment.add_class(self.assigclass) + assignment.add_class(assigclass) N = random.randint(1, 50) val = "".join(random.choices(string.ascii_uppercase + string.digits, k=N)) - with self.assertRaises(ValueError): - self.assignment.set_capacity_field(val) + with pytest.raises(ValueError): + assignment.set_capacity_field(val) + + assignment.set_capacity_field("capacity") + assert assignment.capacity_field == "capacity" + + def test_info(self, assignment: TrafficAssignment, assigclass: TrafficClass): + iterations = random.randint(1, 10000) + rgap = random.random() / 10000 + algo = choice(self.algorithms) + + assignment.add_class(assigclass) + assignment.set_vdf("BPR") + assignment.set_vdf_parameters({"alpha": 0.15, "beta": 4.0}) + assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) + + assignment.set_capacity_field("capacity") + assignment.set_time_field("free_flow_time") + + assignment.max_iter = iterations + assignment.rgap_target = rgap + assignment.set_algorithm(algo) + + # TY + for _ in range(10): + algo = "".join([x.upper() if random.random() < 0.5 else x.lower() for x in algo]) + + dct = assignment.info() + if algo.lower() == "fw": + algo = "frank-wolfe" + assert dct["Algorithm"] == algo.lower(), "Algorithm not correct in info method" + + assert dct["Maximum iterations"] == iterations, "maximum iterations not correct in info method" + - self.assignment.set_capacity_field("capacity") - self.assertEqual(self.assignment.capacity_field, "capacity") +class TestTrafficAssignment: + @pytest.fixture(params=["memmap", "memonly"]) + def matrix(self, request, matrix): + if request.param == "memonly": + return matrix.copy(memory_only=True) + return matrix - def test_execute_and_save_results(self): + def test_execute_and_save_results( + self, assignment: TrafficAssignment, assigclass: TrafficClass, car_graph: Graph, matrix + ): conn = sqlite3.connect(os.path.join(siouxfalls_project, "project_database.sqlite")) results = pd.read_sql("select volume from links order by link_id", conn) - self.assignment.add_class(self.assigclass) - self.assignment.set_vdf("BPR") - self.assignment.set_vdf_parameters({"alpha": 0.15, "beta": 4.0}) - self.assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) + assignment.add_class(assigclass) + assignment.set_vdf("BPR") + assignment.set_vdf_parameters({"alpha": 0.15, "beta": 4.0}) + assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) - self.assignment.set_capacity_field("capacity") - self.assignment.set_time_field("free_flow_time") + assignment.set_capacity_field("capacity") + assignment.set_time_field("free_flow_time") - self.assignment.max_iter = 10 - self.assignment.set_algorithm("msa") - self.assignment.execute() + assignment.max_iter = 10 + assignment.set_algorithm("msa") + assignment.execute() - msa10_rgap = self.assignment.assignment.rgap + msa10_rgap = assignment.assignment.rgap - correl = np.corrcoef(self.assigclass.results.total_link_loads, results.volume.values)[0, 1] - self.assertLess(0.8, correl) + correl = np.corrcoef(assigclass.results.total_link_loads, results.volume.values)[0, 1] + assert 0.8 < correl - self.assignment.max_iter = 500 - self.assignment.rgap_target = 0.001 - self.assignment.set_algorithm("msa") - self.assignment.execute() - msa25_rgap = self.assignment.assignment.rgap + assignment.max_iter = 500 + assignment.rgap_target = 0.001 + assignment.set_algorithm("msa") + assignment.execute() + msa25_rgap = assignment.assignment.rgap - correl = np.corrcoef(self.assigclass.results.total_link_loads, results.volume)[0, 1] - self.assertLess(0.98, correl) + correl = np.corrcoef(assigclass.results.total_link_loads, results.volume)[0, 1] + assert 0.98 < correl - self.assignment.set_algorithm("frank-wolfe") - self.assignment.execute() + assignment.set_algorithm("frank-wolfe") + assignment.execute() - fw25_rgap = self.assignment.assignment.rgap - fw25_iters = self.assignment.assignment.iter + fw25_rgap = assignment.assignment.rgap + fw25_iters = assignment.assignment.iter - correl = np.corrcoef(self.assigclass.results.total_link_loads, results.volume)[0, 1] - self.assertLess(0.99, correl) + correl = np.corrcoef(assigclass.results.total_link_loads, results.volume)[0, 1] + assert 0.99 < correl - self.assignment.set_algorithm("cfw") - self.assignment.execute() - cfw25_rgap = self.assignment.assignment.rgap - cfw25_iters = self.assignment.assignment.iter + assignment.set_algorithm("cfw") + assignment.execute() + cfw25_rgap = assignment.assignment.rgap + cfw25_iters = assignment.assignment.iter - correl = np.corrcoef(self.assigclass.results.total_link_loads, results.volume)[0, 1] - self.assertLess(0.995, correl) + correl = np.corrcoef(assigclass.results.total_link_loads, results.volume)[0, 1] + assert 0.995 < correl # For the last algorithm, we set skimming - self.car_graph.set_skimming(["free_flow_time", "distance"]) - assigclass = TrafficClass("car", self.car_graph, self.matrix) - self.assignment.set_classes([assigclass]) + car_graph.set_skimming(["free_flow_time", "distance"]) + assigclass = TrafficClass("car", car_graph, matrix) + assignment.set_classes([assigclass]) - self.assignment.set_algorithm("bfw") - self.assignment.execute() - bfw25_rgap = self.assignment.assignment.rgap - bfw25_iters = self.assignment.assignment.iter + assignment.set_algorithm("bfw") + assignment.execute() + bfw25_rgap = assignment.assignment.rgap + bfw25_iters = assignment.assignment.iter - correl = np.corrcoef(self.assigclass.results.total_link_loads, results.volume)[0, 1] - self.assertLess(0.999, correl) + correl = np.corrcoef(assigclass.results.total_link_loads, results.volume)[0, 1] + assert 0.999 < correl - self.assertLess(msa25_rgap, msa10_rgap) + assert msa25_rgap < msa10_rgap # MSA and FW do not reach 1e-4 within 500 iterations, cfw and bfw do - self.assertLess(fw25_rgap, msa25_rgap) - self.assertLess(cfw25_rgap, self.assignment.rgap_target) - self.assertLess(bfw25_rgap, self.assignment.rgap_target) + assert fw25_rgap < msa25_rgap + assert cfw25_rgap < assignment.rgap_target + assert bfw25_rgap < assignment.rgap_target # we expect bfw to converge quicker than cfw - self.assertLess(cfw25_iters, fw25_iters) - self.assertLess(bfw25_iters, cfw25_iters) + assert cfw25_iters < fw25_iters + assert bfw25_iters < cfw25_iters - self.assignment.save_results("save_to_database") - self.assignment.save_skims(matrix_name="all_skims", which_ones="all") + assignment.save_results("save_to_database") + assignment.save_skims(matrix_name="all_skims", which_ones="all") - with self.assertRaises(ValueError): - self.assignment.save_results("save_to_database") + with pytest.raises(ValueError): + assignment.save_results("save_to_database") - def test_execute_no_project(self): + def test_execute_no_project(self, project: Project, assignment: TrafficAssignment, assigclass: TrafficClass): conn = sqlite3.connect(os.path.join(siouxfalls_project, "project_database.sqlite")) results = pd.read_sql("select volume from links order by link_id", conn) - self.project.close() + project.close() assignment = TrafficAssignment() - assignment.add_class(self.assigclass) + assignment.add_class(assigclass) assignment.set_vdf("BPR") assignment.set_vdf_parameters({"alpha": 0.15, "beta": 4.0}) assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) @@ -230,36 +281,8 @@ def test_execute_no_project(self): assignment.set_algorithm("msa") assignment.execute() - correl = np.corrcoef(self.assigclass.results.total_link_loads, results.volume.values)[0, 1] - self.assertLess(0.8, correl) + correl = np.corrcoef(assigclass.results.total_link_loads, results.volume.values)[0, 1] + assert 0.8 < correl - with self.assertRaises(FileNotFoundError): + with pytest.raises(FileNotFoundError): assignment.save_results("anything") - - def test_info(self): - iterations = random.randint(1, 10000) - rgap = random.random() / 10000 - algo = choice(self.algorithms) - - self.assignment.add_class(self.assigclass) - self.assignment.set_vdf("BPR") - self.assignment.set_vdf_parameters({"alpha": 0.15, "beta": 4.0}) - self.assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) - - self.assignment.set_capacity_field("capacity") - self.assignment.set_time_field("free_flow_time") - - self.assignment.max_iter = iterations - self.assignment.rgap_target = rgap - self.assignment.set_algorithm(algo) - - # TY - for _ in range(10): - algo = "".join([x.upper() if random.random() < 0.5 else x.lower() for x in algo]) - - dct = self.assignment.info() - if algo.lower() == "fw": - algo = "frank-wolfe" - self.assertEqual(dct["Algorithm"], algo.lower(), "Algorithm not correct in info method") - - self.assertEqual(dct["Maximum iterations"], iterations, "maximum iterations not correct in info method") From 6a21f02a218f3546b39126077cfaa055d62ad0f4 Mon Sep 17 00:00:00 2001 From: Pedro Camargo Date: Mon, 3 Jul 2023 01:18:00 -0700 Subject: [PATCH 5/9] 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 --- aequilibrae/paths/network_skimming.py | 39 ++++++++++++++++--- aequilibrae/paths/results/skim_results.py | 28 +------------ docs/source/conf.py | 1 + .../plot_forecasting.py | 0 .../source/examples/full_workflows/readme.rst | 4 ++ 5 files changed, 40 insertions(+), 32 deletions(-) rename docs/source/examples/{aequilibrae_without_a_model => full_workflows}/plot_forecasting.py (100%) create mode 100644 docs/source/examples/full_workflows/readme.rst diff --git a/aequilibrae/paths/network_skimming.py b/aequilibrae/paths/network_skimming.py index 4d9b6f9e0..9745fa52e 100644 --- a/aequilibrae/paths/network_skimming.py +++ b/aequilibrae/paths/network_skimming.py @@ -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 @@ -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 = [] @@ -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) @@ -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 diff --git a/aequilibrae/paths/results/skim_results.py b/aequilibrae/paths/results/skim_results.py index e522175ae..5efcd1d07 100644 --- a/aequilibrae/paths/results/skim_results.py +++ b/aequilibrae/paths/results/skim_results.py @@ -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 @@ -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") diff --git a/docs/source/conf.py b/docs/source/conf.py index 2073dde09..1367a2f8a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -79,6 +79,7 @@ "examples/trip_distribution", "examples/visualization", "examples/aequilibrae_without_a_model", + "examples/full_workflows", "examples/other_applications"]) } diff --git a/docs/source/examples/aequilibrae_without_a_model/plot_forecasting.py b/docs/source/examples/full_workflows/plot_forecasting.py similarity index 100% rename from docs/source/examples/aequilibrae_without_a_model/plot_forecasting.py rename to docs/source/examples/full_workflows/plot_forecasting.py diff --git a/docs/source/examples/full_workflows/readme.rst b/docs/source/examples/full_workflows/readme.rst new file mode 100644 index 000000000..ee5963096 --- /dev/null +++ b/docs/source/examples/full_workflows/readme.rst @@ -0,0 +1,4 @@ +.. _full_workflows: + +Full Workflows +-------------- From 08072245f8dfae8adad5dfb05c4538632c1cfabf Mon Sep 17 00:00:00 2001 From: Renata Imai <53949163+r-akemii@users.noreply.github.com> Date: Wed, 5 Jul 2023 11:26:52 -0300 Subject: [PATCH 6/9] 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 Co-authored-by: Pedro Camargo --- .coveragerc | 3 + .../workflows/test_linux_with_coverage.yml | 16 +-- aequilibrae/paths/traffic_assignment.py | 27 ++++- aequilibrae/paths/traffic_class.py | 40 ++++++- .../other_applications/plot_check_logging.py | 102 ++++++++++++++++++ .../paths/test_traffic_assignment.py | 55 +++++++++- 6 files changed, 219 insertions(+), 24 deletions(-) create mode 100644 .coveragerc create mode 100644 docs/source/examples/other_applications/plot_check_logging.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..3a2a8f12d --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[report] +fail_under = 81.0 +show_missing = True diff --git a/.github/workflows/test_linux_with_coverage.yml b/.github/workflows/test_linux_with_coverage.yml index e8b4a2f47..5df7ce9c7 100644 --- a/.github/workflows/test_linux_with_coverage.yml +++ b/.github/workflows/test_linux_with_coverage.yml @@ -1,8 +1,6 @@ name: Code coverage -on: - pull_request: - types: [ready_for_review, merge] +on: [pull_request] jobs: testing: @@ -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 \ No newline at end of file + python3 -m pytest --cov=aequilibrae tests/ \ No newline at end of file diff --git a/aequilibrae/paths/traffic_assignment.py b/aequilibrae/paths/traffic_assignment.py index 860ccb86a..15e9633dd 100644 --- a/aequilibrae/paths/traffic_assignment.py +++ b/aequilibrae/paths/traffic_assignment.py @@ -1,4 +1,6 @@ +from copy import deepcopy import importlib.util as iutil +import logging import socket import sqlite3 from datetime import datetime @@ -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"] @@ -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: @@ -248,6 +255,9 @@ def set_algorithm(self, algorithm: str): 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: """ @@ -265,6 +275,7 @@ def set_vdf_parameters(self, par: dict) -> None: "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"]: @@ -291,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 @@ -365,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: """ @@ -391,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 @@ -410,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 diff --git a/aequilibrae/paths/traffic_class.py b/aequilibrae/paths/traffic_class.py index 68bd15fc5..aaf3451e8 100644 --- a/aequilibrae/paths/traffic_class.py +++ b/aequilibrae/paths/traffic_class.py @@ -1,11 +1,12 @@ +import warnings +from copy import deepcopy from typing import Union, List, Tuple, Dict + import numpy as np -import pandas as pd -from aequilibrae.paths.graph import Graph from aequilibrae.matrix import AequilibraeMatrix +from aequilibrae.paths.graph import Graph from aequilibrae.paths.results import AssignmentResults -import warnings class TrafficClass: @@ -51,7 +52,7 @@ def __init__(self, name: str, graph: Graph, matrix: AequilibraeMatrix) -> None: if matrix.matrix_view.dtype != graph.default_types("float"): raise TypeError("Matrix's computational view need to be of type np.float64") - + self.__config = {} self.graph = graph self.logger = graph.logger self.matrix = matrix @@ -67,6 +68,30 @@ def __init__(self, name: str, graph: Graph, matrix: AequilibraeMatrix) -> None: self._selected_links = {} # maps human name to link_set self.__id__ = name + graph_config = { + "Mode": graph.mode, + "Block through centroids": graph.block_centroid_flows, + "Number of centroids": graph.num_zones, + "Links": graph.num_links, + "Nodes": graph.num_nodes, + } + self.__config["Graph"] = str(graph_config) + + mat_config = { + "Source": matrix.file_path or "", + "Number of centroids": matrix.zones, + "Matrix cores": matrix.view_names, + } + if len(matrix.view_names) == 1: + mat_config["Matrix totals"] = { + nm: np.sum(np.nan_to_num(matrix.matrix_view)[:, :]) for nm in matrix.view_names + } + else: + mat_config["Matrix totals"] = { + nm: np.sum(np.nan_to_num(matrix.matrix_view)[:, :, i]) for i, nm in enumerate(matrix.view_names) + } + self.__config["Matrix"] = str(mat_config) + def set_pce(self, pce: Union[float, int]) -> None: """Sets Passenger Car equivalent @@ -140,6 +165,12 @@ def set_select_links(self, links: Dict[str, List[Tuple[int, int]]]): else: link_ids.append(comp_id) self._selected_links[name] = np.array(link_ids, dtype=self.graph.default_types("int")) + self.__config["select_links"] = str(links) + + @property + def info(self) -> dict: + config = deepcopy(self.__config) + return {self.__id__: config} def __setattr__(self, key, value): if key not in [ @@ -157,6 +188,7 @@ def __setattr__(self, key, value): "fc_multiplier", "fixed_cost_field", "_selected_links", + "_TrafficClass__config", ]: raise KeyError("Traffic Class does not have that element") self.__dict__[key] = value diff --git a/docs/source/examples/other_applications/plot_check_logging.py b/docs/source/examples/other_applications/plot_check_logging.py new file mode 100644 index 000000000..c058534e6 --- /dev/null +++ b/docs/source/examples/other_applications/plot_check_logging.py @@ -0,0 +1,102 @@ +""" +.. _useful-log-tips: + +Checking AequilibraE's log +========================== + +AequilibraE's log is a very useful tool to get more information about +what the software is doing under the hood. + +Information such as Traffic Class and Traffic Assignment stats, and Traffic Assignment +outputs. If you have created your project's network from OSM, you will also find +information on the number of nodes, links, and the query performed to obtain the data. + +In this example, we'll use Sioux Falls data to check the logs, but we strongly encourage +you to go ahead and download a place of your choice and perform a traffic assignment! +""" +# %% +# Imports +from uuid import uuid4 +from tempfile import gettempdir +from os.path import join +from aequilibrae.utils.create_example import create_example +from aequilibrae.paths import TrafficAssignment, TrafficClass + +# %% +# We create an empty project on an arbitrary folder +fldr = join(gettempdir(), uuid4().hex) +project = create_example(fldr) + +# %% +# We build our graphs +project.network.build_graphs() + +graph = project.network.graphs["c"] +graph.set_graph("free_flow_time") +graph.set_skimming(["free_flow_time", "distance"]) +graph.set_blocked_centroid_flows(False) + +# %% +# We get our demand matrix from the project and create a computational view +proj_matrices = project.matrices +demand = proj_matrices.get_matrix("demand_omx") +demand.computational_view(["matrix"]) + +# %% +# Now let's perform our traffic assignment +assig = TrafficAssignment() + +assigclass = TrafficClass(name="car", graph=graph, matrix=demand) + +assig.add_class(assigclass) +assig.set_vdf("BPR") +assig.set_vdf_parameters({"alpha": 0.15, "beta": 4.0}) +assig.set_capacity_field("capacity") +assig.set_time_field("free_flow_time") +assig.set_algorithm("bfw") +assig.max_iter = 50 +assig.rgap_target = 0.001 + +assig.execute() + +# %% +# +with open(join(fldr, "aequilibrae.log")) as file: + for idx, line in enumerate(file): + print(idx + 1, "-", line) + +# %% +# In lines 1-7, we receive some warnings that our fields name and lane have ``NaN`` values. +# As they are not relevant to our example, we can move on. +# +# In lines 8-9 we get the Traffic Class specifications. +# We can see that there is only one traffic class (car). Its **graph** key presents information +# on blocked flow through centroids, number of centroids, links, and nodes. +# In the **matrix** key, we find information on where in the disk the matrix file is located. +# We also have information on the number of centroids and nodes, as well as on the matrix/matrices +# used for computation. In our example, we only have one matrix named matrix, and the total +# sum of this matrix element is equal to 360,600. If you have more than one matrix its data +# will be also displayed in the *matrix_cores* and *matrix_totals* keys. +# +# In lines 10-11 the log shows the Traffic Assignment specifications. +# We can see that the VDF parameters, VDF function, capacity and time fields, algorithm, +# maximum number of iterations, and target gap are just like the ones we set previously. +# The only information that might be new to you is the number of cores used for computation. +# If you haven't set any, AequilibraE is going to use the largest number of CPU threads +# available. +# +# Line 12 displays us a warning to indicate that AequilibraE is converting the data type +# of the cost field. +# +# Lines 13-61 indicate that we'll receive the outputs of a *bfw* algorithm. +# In the log there are also the number of the iteration, its relative gap, and the stepsize. +# The outputs in lines 15-60 are exactly the same as the ones provided by the function +# ``assig.report()``. Finally, the last line shows us that the *bfw* assignment has finished +# after 46 iterations because its gap is smaller than the threshold we configured (0.001). +# +# In case you execute a new traffic assignment using different classes or changing the +# parameters values, these new specification values would be stored in the log file as well +# so you can always keep a record of what you have been doing. One last reminder is that +# if we had created our project from OSM, the lines on top of the log would have been +# different to display information on the queries done to the server to obtain the data. +# diff --git a/tests/aequilibrae/paths/test_traffic_assignment.py b/tests/aequilibrae/paths/test_traffic_assignment.py index f3f473612..1af80a722 100644 --- a/tests/aequilibrae/paths/test_traffic_assignment.py +++ b/tests/aequilibrae/paths/test_traffic_assignment.py @@ -1,15 +1,15 @@ -import os import random import sqlite3 import string +from os.path import join, isfile +from pathlib import Path from random import choice import numpy as np import pandas as pd import pytest -from aequilibrae import TrafficAssignment, TrafficClass, Graph -from aequilibrae.project.project import Project +from aequilibrae import TrafficAssignment, TrafficClass, Graph, Project from aequilibrae.utils.create_example import create_example from ...data import siouxfalls_project @@ -188,9 +188,10 @@ def matrix(self, request, matrix): def test_execute_and_save_results( self, assignment: TrafficAssignment, assigclass: TrafficClass, car_graph: Graph, matrix ): - conn = sqlite3.connect(os.path.join(siouxfalls_project, "project_database.sqlite")) + conn = sqlite3.connect(join(siouxfalls_project, "project_database.sqlite")) results = pd.read_sql("select volume from links order by link_id", conn) + proj = assignment.project assignment.add_class(assigclass) assignment.set_vdf("BPR") assignment.set_vdf_parameters({"alpha": 0.15, "beta": 4.0}) @@ -262,8 +263,52 @@ def test_execute_and_save_results( with pytest.raises(ValueError): assignment.save_results("save_to_database") + num_cores = assignment.cores + # Let's test logging of assignment + log_ = Path(proj.path_to_file).parent / "aequilibrae.log" + assert isfile(log_) + + file_text = "" + with open(log_, "r", encoding="utf-8") as file: + for line in file.readlines(): + file_text += line + + tc_spec = "INFO ; Traffic Class specification" + assert file_text.count(tc_spec) > 1 + + tc_graph = "INFO ; {'car': {'Graph': \"{'Mode': 'c', 'Block through centroids': False, 'Number of centroids': 24, 'Links': 76, 'Nodes': 24}\"," + assert file_text.count(tc_graph) > 1 + + tc_matrix = "'Number of centroids': 24, 'Matrix cores': ['matrix'], 'Matrix totals': {'matrix': 360600.0}}\"}}" + assert file_text.count(tc_matrix) > 1 + + assig_1 = "INFO ; {{'VDF parameters': {{'alpha': 'b', 'beta': 'power'}}, 'VDF function': 'bpr', 'Number of cores': {}, 'Capacity field': 'capacity', 'Time field': 'free_flow_time', 'Algorithm': 'msa', 'Maximum iterations': 10, 'Target RGAP': 0.0001}}".format( + num_cores + ) + assert assig_1 in file_text + + assig_2 = "INFO ; {{'VDF parameters': {{'alpha': 'b', 'beta': 'power'}}, 'VDF function': 'bpr', 'Number of cores': {}, 'Capacity field': 'capacity', 'Time field': 'free_flow_time', 'Algorithm': 'msa', 'Maximum iterations': 500, 'Target RGAP': 0.001}}".format( + num_cores + ) + assert assig_2 in file_text + + assig_3 = "INFO ; {{'VDF parameters': {{'alpha': 'b', 'beta': 'power'}}, 'VDF function': 'bpr', 'Number of cores': {}, 'Capacity field': 'capacity', 'Time field': 'free_flow_time', 'Algorithm': 'frank-wolfe', 'Maximum iterations': 500, 'Target RGAP': 0.001}}".format( + num_cores + ) + assert assig_3 in file_text + + assig_4 = "INFO ; {{'VDF parameters': {{'alpha': 'b', 'beta': 'power'}}, 'VDF function': 'bpr', 'Number of cores': {}, 'Capacity field': 'capacity', 'Time field': 'free_flow_time', 'Algorithm': 'cfw', 'Maximum iterations': 500, 'Target RGAP': 0.001}}".format( + num_cores + ) + assert assig_4 in file_text + + assig_5 = "INFO ; {{'VDF parameters': {{'alpha': 'b', 'beta': 'power'}}, 'VDF function': 'bpr', 'Number of cores': {}, 'Capacity field': 'capacity', 'Time field': 'free_flow_time', 'Algorithm': 'bfw', 'Maximum iterations': 500, 'Target RGAP': 0.001}}".format( + num_cores + ) + assert assig_5 in file_text + def test_execute_no_project(self, project: Project, assignment: TrafficAssignment, assigclass: TrafficClass): - conn = sqlite3.connect(os.path.join(siouxfalls_project, "project_database.sqlite")) + conn = sqlite3.connect(join(siouxfalls_project, "project_database.sqlite")) results = pd.read_sql("select volume from links order by link_id", conn) project.close() From 6a2088c3f54d46f66b204c0e85016c002fad7ab9 Mon Sep 17 00:00:00 2001 From: Pedro Camargo Date: Wed, 5 Jul 2023 07:27:19 -0700 Subject: [PATCH 7/9] narrows coverage test (#429) Co-authored-by: pveigadecamargo --- aequilibrae/project/network/gmns_exporter.py | 45 ++++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/aequilibrae/project/network/gmns_exporter.py b/aequilibrae/project/network/gmns_exporter.py index 6c7e35ffb..85251fc8b 100644 --- a/aequilibrae/project/network/gmns_exporter.py +++ b/aequilibrae/project/network/gmns_exporter.py @@ -48,39 +48,28 @@ def doWork(self): def update_direction_field(self): two_way_cols = list(set([col[:-3] for col in list(self.links_df.columns) if col[-3:] in ["_ab", "_ba"]])) - for idx, row in self.links_df.iterrows(): - if row.direction == 0: - self.links_df = pd.concat([self.links_df, self.links_df.loc[idx:idx, :]], axis=0) - self.links_df.reset_index(drop=True, inplace=True) - self.links_df.loc[self.links_df.index[-1], "link_id"] = max(list(self.links_df.link_id)) + 1 - self.links_df.loc[self.links_df.index[-1], "a_node"] = row.b_node - self.links_df.loc[self.links_df.index[-1], "b_node"] = row.a_node + ab_links = pd.DataFrame(self.links_df[self.links_df.direction > -1], copy=True) + ba_links = pd.DataFrame(self.links_df[self.links_df.direction < 1], copy=True) - self.links_df.loc[self.links_df.index[-1], "direction"] = 1 - self.links_df.loc[idx, "direction"] = 1 + # treats ab_links and bi-directionals + if ab_links.shape[0]: + ab_links.loc[:, "dir_flag"] = 1 + for col in two_way_cols: + ab_links.loc[:, col] = ab_links.loc[:, col + "_ab"] - self.links_df.loc[self.links_df.index[-1], "dir_flag"] = -1 - self.links_df.loc[idx, "dir_flag"] = 1 + # treats ba_links and bi-directionals + if ba_links.shape[0]: + ba_links.loc[:, "direction"] = 1 + ba_links.loc[:, "dir_flag"] = -1 + b = ba_links.b_node.to_numpy() + ba_links.loc[:, "b_node"] = ba_links.a_node.to_numpy()[:] + ba_links.loc[:, "a_node"] = b[:] - for col in two_way_cols: - self.links_df.loc[idx, col] = self.links_df.loc[idx, col + "_ab"] - self.links_df.loc[self.links_df.index[-1], col] = self.links_df.loc[idx, col + "_ba"] + for col in two_way_cols: + ba_links.loc[:, col] = ba_links.loc[:, col + "_ba"] - elif row.direction == -1: - for col in two_way_cols: - self.links_df.loc[idx, col] = self.links_df.loc[idx, col + "_ba"] - - self.links_df.loc[idx, "a_node"] = row.b_node - self.links_df.loc[idx, "b_node"] = row.a_node - self.links_df.loc[idx, "direction"] = 1 - self.links_df.loc[idx, "dir_flag"] = -1 - - else: - for col in two_way_cols: - self.links_df.loc[idx, col] = self.links_df.loc[idx, col + "_ab"] - - self.links_df.loc[idx, "dir_flag"] = 1 + self.links_df = pd.concat([ab_links, ba_links]) def update_field_names(self): """ From 9580ec6b1a6393a72aa09fe0e3570f0d4ab57aaf Mon Sep 17 00:00:00 2001 From: Pedro Camargo Date: Sun, 9 Jul 2023 18:46:14 -0700 Subject: [PATCH 8/9] 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 --- .github/workflows/build_artifacts_qgis.yml | 2 +- requirements.txt | 3 ++- requirements_additional.txt | 3 +-- setup.py | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_artifacts_qgis.yml b/.github/workflows/build_artifacts_qgis.yml index cf6b6ad7f..2f5bbc338 100644 --- a/.github/workflows/build_artifacts_qgis.yml +++ b/.github/workflows/build_artifacts_qgis.yml @@ -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: diff --git a/requirements.txt b/requirements.txt index 7c3dd56c5..6a1ba0519 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ requests shapely pandas pyproj -rtree \ No newline at end of file +rtree +openmatrix \ No newline at end of file diff --git a/requirements_additional.txt b/requirements_additional.txt index 4f5426d72..96f666a5e 100644 --- a/requirements_additional.txt +++ b/requirements_additional.txt @@ -1,3 +1,2 @@ pyarrow -PyQt5 -openmatrix \ No newline at end of file +PyQt5 \ No newline at end of file diff --git a/setup.py b/setup.py index bc0c5d0f8..a52ccfda1 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ import importlib.util as iutil -import os import platform -from os.path import dirname, join +from os.path import join import numpy as np from Cython.Distutils import build_ext From e5a21f1fea76ae8e6c0ea74f40ef7c8d06108e25 Mon Sep 17 00:00:00 2001 From: Pedro Camargo Date: Sun, 9 Jul 2023 21:43:29 -0700 Subject: [PATCH 9/9] Updates software version for release * Bumps up version * Bumps up documentation version --------- Co-authored-by: pveigadecamargo --- __version__.py | 2 +- docs/source/_static/switcher.json | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/__version__.py b/__version__.py index 30bc3d28b..ad1d569bc 100644 --- a/__version__.py +++ b/__version__.py @@ -1,5 +1,5 @@ version = 0.9 -minor_version = "2" +minor_version = "3" release_name = "Queluz" release_version = f"{version}.{minor_version}" diff --git a/docs/source/_static/switcher.json b/docs/source/_static/switcher.json index d266c4584..f5a119ce3 100644 --- a/docs/source/_static/switcher.json +++ b/docs/source/_static/switcher.json @@ -4,6 +4,11 @@ "version": "develop", "url": "https://aequilibrae.com/python/develop/" }, + { + "name": "0.9.3", + "version": "0.9.3", + "url": "https://aequilibrae.com/python/V.0.9.3/" + }, { "name": "0.9.2", "version": "0.9.2",