Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates assignment logging #423

Merged
merged 17 commits into from
Jul 5, 2023
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
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/
27 changes: 26 additions & 1 deletion 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 @@ -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:
"""
Expand All @@ -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"]:
Expand All @@ -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
Expand Down Expand Up @@ -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:
"""
Expand All @@ -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
Expand All @@ -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

Expand Down
40 changes: 36 additions & 4 deletions aequilibrae/paths/traffic_class.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 [
Expand All @@ -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
102 changes: 102 additions & 0 deletions docs/source/examples/other_applications/plot_check_logging.py
Original file line number Diff line number Diff line change
@@ -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.
#
55 changes: 50 additions & 5 deletions tests/aequilibrae/paths/test_traffic_assignment.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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, 'Nodes': 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()
Expand Down