def test_skimming_on_assignment(self): matrix = AequilibraeMatrix() matrix.load(os.path.join(gettempdir(), self.mat_name)) matrix.computational_view(["cars"]) res = AssignmentResults() res.prepare(self.g, matrix) self.g.set_skimming([]) self.g.set_blocked_centroid_flows(True) assig = allOrNothing(matrix, self.g, res) assig.execute() if res.skims.distance.sum() > 0: self.fail( "skimming for nothing during assignment returned something different than zero" ) self.g.set_skimming("distance") res.prepare(self.g, matrix) assig = allOrNothing(matrix, self.g, res) assig.execute() if res.skims.distance.sum() != 2914644.0: self.fail("skimming during assignment returned the wrong value") matrix.close()
def __setCriticalLinks(self, save=False, queries={}, crit_res_result=None): a = AequilibraeMatrix() if save: if crit_res_result is None: warnings.warn( "Critical Link analysis not set properly. Need to specify output file too" ) else: if crit_res_result[-3:].lower() != "aem": crit_res_result += ".aes" if self.nodes > 0 and self.zones > 0: if ["elements", "labels", "type"] in queries.keys(): if len(queries["labels"]) == len( queries["elements"]) == len(queries["type"]): a.create_empty(file_name=crit_res_result, zones=self.zones, matrix_names=queries["labels"]) else: raise ValueError( "Queries are inconsistent. 'Labels', 'elements' and 'type' need to have same dimensions" ) else: raise ValueError( "Queries are inconsistent. It needs to contain the following elements: 'Labels', 'elements' and 'type'" ) else: a.create_empty(file_name=a.random_name(), zones=self.zones, matrix_names=["empty", "nothing"]) a.computational_view() if len(a.matrix_view.shape[:]) == 2: a.matrix_view = a.matrix_view.reshape((self.zones, self.zones, 1)) self.critical_links = {"save": save, "queries": queries, "results": a}
def test_set_pce(self): mat_name = AequilibraeMatrix().random_name() g = Graph() g.load_from_disk(test_graph) g.set_graph(cost_field="distance") # Creates the matrix for assignment args = { "file_name": os.path.join(gettempdir(), mat_name), "zones": g.num_zones, "matrix_names": ["cars", "trucks"], "index_names": ["my indices"], } matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.index[:] = g.centroids[:] matrix.cars.fill(1.1) matrix.trucks.fill(2.2) matrix.computational_view() tc = TrafficClass(graph=g, matrix=matrix) self.assertIsInstance(tc.results, AssignmentResults, 'Results have the wrong type') self.assertIsInstance(tc._aon_results, AssignmentResults, 'Results have the wrong type') with self.assertRaises(ValueError): tc.set_pce('not a number') tc.set_pce(1) tc.set_pce(3.9)
def test_execute(self): # Loads and prepares the graph car_loads = [] two_class_loads = [] for extension in ["omx", "aem"]: matrix = AequilibraeMatrix() if extension == 'omx': mat_name = os.path.join(gettempdir(), "my_matrix." + extension) else: mat_name = self.mat_name matrix.load(mat_name) matrix.computational_view(["cars"]) # Performs assignment res = AssignmentResults() res.prepare(self.g, matrix) assig = allOrNothing(matrix, self.g, res) assig.execute() car_loads.append(res.link_loads) res.save_to_disk( os.path.join(gettempdir(), "link_loads_{}.aed".format(extension))) res.save_to_disk( os.path.join(gettempdir(), "link_loads_{}.csv".format(extension))) matrix.computational_view() # Performs assignment res = AssignmentResults() res.prepare(self.g, matrix) assig = allOrNothing(matrix, self.g, res) assig.execute() two_class_loads.append(res.link_loads) res.save_to_disk( os.path.join(gettempdir(), "link_loads_2_classes_{}.aed".format(extension))) res.save_to_disk( os.path.join(gettempdir(), "link_loads_2_classes_{}.csv".format(extension))) matrix.close() load_diff = two_class_loads[0] - two_class_loads[1] if load_diff.max() > 0.0000000001 or load_diff.max() < -0.0000000001: self.fail( "Loads for two classes differ for OMX and AEM matrix types") load_diff = car_loads[0] - car_loads[1] if load_diff.max() > 0.0000000001 or load_diff.max() < -0.0000000001: self.fail( "Loads for a single class differ for OMX and AEM matrix types")
def test_save(self): a = AequilibraeMatrix() a.load(self.sf_skims) a.computational_view(['distance']) new_mat = np.random.rand(a.zones, a.zones) a.matrix_view *= new_mat res = a.matrix_view.sum() a.save('new_name_for_matrix') self.assertEqual(res, a.matrix_view.sum(), 'Saved wrong result') a.save(['new_name_for_matrix2']) self.assertEqual(a.view_names[0], 'new_name_for_matrix2', 'Did not update computational view') self.assertEqual(len(a.view_names), 1, 'computational view with the wrong number of matrices') a.computational_view(['distance', 'new_name_for_matrix']) with self.assertRaises(ValueError): a.save(['just_one_name']) a.save(['one_name', 'two_names']) with self.assertRaises(ValueError): a.save('distance') b = AequilibraeMatrix() b.load(self.name_test) b.computational_view("seed") b.save() b.computational_view(["mat", "seed", "dist"]) b.save()
def test_save(self): a = AequilibraeMatrix() a.load(self.sf_skims) a.computational_view(["distance"]) new_mat = np.random.rand(a.zones, a.zones) a.matrix_view *= new_mat res = a.matrix_view.sum() a.save("new_name_for_matrix") self.assertEqual(res, a.matrix_view.sum(), "Saved wrong result") a.save(["new_name_for_matrix2"]) self.assertEqual(a.view_names[0], "new_name_for_matrix2", "Did not update computational view") self.assertEqual(len(a.view_names), 1, "computational view with the wrong number of matrices") a.computational_view(["distance", "new_name_for_matrix"]) with self.assertRaises(ValueError): a.save(["just_one_name"]) a.save(["one_name", "two_names"]) with self.assertRaises(ValueError): a.save("distance") b = AequilibraeMatrix() b.load(self.name_test) b.computational_view("seed") b.save() b.computational_view(["mat", "seed", "dist"]) b.save()
def test_execute(self): # Loads and prepares the graph g = Graph() g.load_from_disk(test_graph) g.set_graph(cost_field='distance', skim_fields=None) # None implies that only the cost field will be skimmed # Prepares the matrix for assignment args = { 'file_name': '/tmp/my_matrix.aem', 'zones': g.num_zones, 'matrix_names': ['cars', 'trucks'], 'index_names': ['my indices'] } matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.index[:] = g.centroids[:] matrix.cars.fill(1) matrix.trucks.fill(2) matrix.computational_view(['cars']) # Performs assignment res = AssignmentResults() res.prepare(g, matrix) assig = allOrNothing(matrix, g, res) assig.execute() res.save_to_disk('/tmp/link_loads.aed') res.save_to_disk('/tmp/link_loads.csv') matrix.computational_view() # Performs assignment res = AssignmentResults() res.prepare(g, matrix) assig = allOrNothing(matrix, g, res) assig.execute() res.save_to_disk('/tmp/link_loads_2_classes.aed') res.save_to_disk('/tmp/link_loads_2_classes.csv')
def test_execute(self): # Loads and prepares the graph g = Graph() g.load_from_disk(test_graph) g.set_graph(cost_field="distance", skim_fields=None) # None implies that only the cost field will be skimmed # Prepares the matrix for assignment args = { "file_name": os.path.join(gettempdir(), "my_matrix.aem"), "zones": g.num_zones, "matrix_names": ["cars", "trucks"], "index_names": ["my indices"], } matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.index[:] = g.centroids[:] matrix.cars.fill(1) matrix.trucks.fill(2) matrix.computational_view(["cars"]) # Performs assignment res = AssignmentResults() res.prepare(g, matrix) assig = allOrNothing(matrix, g, res) assig.execute() res.save_to_disk(os.path.join(gettempdir(), "link_loads.aed")) res.save_to_disk(os.path.join(gettempdir(), "link_loads.csv")) matrix.computational_view() # Performs assignment res = AssignmentResults() res.prepare(g, matrix) assig = allOrNothing(matrix, g, res) assig.execute() res.save_to_disk(os.path.join(gettempdir(), "link_loads_2_classes.aed")) res.save_to_disk(os.path.join(gettempdir(), "link_loads_2_classes.csv"))
def test_execute(self): # Loads and prepares the graph g = Graph() g.load_from_disk(test_graph) g.set_graph(cost_field='distance', skim_fields=None) # None implies that only the cost field will be skimmed # Prepares the matrix for assignment args = {'file_name': os.path.join(gettempdir(),'my_matrix.aem'), 'zones': g.num_zones, 'matrix_names': ['cars', 'trucks'], 'index_names': ['my indices']} matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.index[:] = g.centroids[:] matrix.cars.fill(1) matrix.trucks.fill(2) matrix.computational_view(['cars']) # Performs assignment res = AssignmentResults() res.prepare(g, matrix) assig = allOrNothing(matrix, g, res) assig.execute() res.save_to_disk(os.path.join(gettempdir(),'link_loads.aed')) res.save_to_disk(os.path.join(gettempdir(),'link_loads.csv')) matrix.computational_view() # Performs assignment res = AssignmentResults() res.prepare(g, matrix) assig = allOrNothing(matrix, g, res) assig.execute() res.save_to_disk(os.path.join(gettempdir(),'link_loads_2_classes.aed')) res.save_to_disk(os.path.join(gettempdir(),'link_loads_2_classes.csv'))
def test_calibrate_with_omx(self): imped = AequilibraeMatrix() imped.load(siouxfalls_skims) imped.computational_view(['free_flow_time']) mat = AequilibraeMatrix() mat.load(siouxfalls_demand) mat.computational_view() args = {"impedance": imped, "matrix": mat, "function": "power", "nan_to_zero": False} distributed_matrix = GravityCalibration(**args) distributed_matrix.calibrate() if distributed_matrix.gap > 0.0001: self.fail("Calibration did not converge") args = {"impedance": imped, "matrix": mat, "function": "power", "nan_to_zero": True} distributed_matrix = GravityCalibration(**args) distributed_matrix.calibrate() if distributed_matrix.gap > 0.0001: self.fail("Calibration did not converge")
import os, tempfile zones = 100 # Impedance matrix_procedures name_test = AequilibraeMatrix().random_name() args = {'file_name': name_test, 'zones': zones, 'matrix_names': ['impedance']} impedance = AequilibraeMatrix() impedance.create_empty(**args) impedance.impedance[:, :] = np.random.rand(zones, zones)[:,:] * 1000 impedance.index[:] = np.arange(impedance.zones) + 100 impedance.computational_view(['impedance']) args['matrix_names'] = ['base_matrix'] matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.base_matrix[:, :] = np.random.rand(zones, zones)[:,:] * 1000 matrix.index[:] = np.arange(matrix.zones) + 100 matrix.computational_view(['base_matrix']) class TestGravityCalibration(TestCase): def test_calibrate(self): args = {'impedance': impedance, 'matrix': matrix, 'function': 'power',
class TestAequilibraeMatrix(TestCase): matrix = None def setUp(self) -> None: self.sf_skims = f"/Aequilibrae_matrix_{uuid.uuid4()}.omx" copyfile(siouxfalls_skims, self.sf_skims) temp_folder = gettempdir() self.name_test = temp_folder + f"/Aequilibrae_matrix_{uuid.uuid4()}.aem" self.copy_matrix_name = temp_folder + f"/Aequilibrae_matrix_{uuid.uuid4()}.aem" self.csv_export_name = temp_folder + f"/Aequilibrae_matrix_{uuid.uuid4()}.csv" self.omx_export_name = temp_folder + f"/Aequilibrae_matrix_{uuid.uuid4()}.omx" if self.matrix is not None: return args = { "file_name": self.name_test, "zones": zones, "matrix_names": ["mat", "seed", "dist"], "index_names": ["my indices"], } self.matrix = AequilibraeMatrix() self.matrix.create_empty(**args) self.matrix.index[:] = np.arange(self.matrix.zones) + 100 self.matrix.mat[:, :] = np.random.rand(self.matrix.zones, self.matrix.zones)[:, :] self.matrix.mat[:, :] = self.matrix.mat[:, :] * (1000 / np.sum(self.matrix.mat[:, :])) self.matrix.setName("Test matrix - " + str(random.randint(1, 10))) self.matrix.setDescription("Generated at " + datetime.datetime.now().strftime("%I:%M%p on %B %d, %Y")) self.new_matrix = self.matrix def tearDown(self) -> None: try: del self.matrix os.remove(self.name_test) if os.path.exists(self.name_test) else None os.remove(self.csv_export_name) if os.path.exists(self.csv_export_name) else None os.remove(self.copy_matrix_name) if os.path.exists(self.copy_matrix_name) else None os.remove(self.omx_export_name) if os.path.exists(self.omx_export_name) else None except Exception as e: print(f"Could not delete. {e.args}") def test_load(self): self.new_matrix = AequilibraeMatrix() # Cannot load OMX file with no indices with self.assertRaises(LookupError): self.new_matrix.load(no_index_omx) self.new_matrix = AequilibraeMatrix() self.new_matrix.load(self.name_test) del self.new_matrix def test_computational_view(self): self.new_matrix.computational_view(["mat", "seed"]) self.new_matrix.mat.fill(0) self.new_matrix.seed.fill(0) if self.new_matrix.matrix_view.shape[2] != 2: self.fail("Computational view returns the wrong number of matrices") self.new_matrix.computational_view(["mat"]) self.new_matrix.matrix_view[:, :] = np.arange(zones ** 2).reshape(zones, zones) if np.sum(self.new_matrix.mat) != np.sum(self.new_matrix.matrix_view): self.fail("Assigning to matrix view did not work") self.new_matrix.setName("Test matrix - " + str(random.randint(1, 10))) self.new_matrix.setDescription("Generated at " + datetime.datetime.now().strftime("%I:%M%p on %B %d, %Y")) del self.new_matrix def test_computational_view_with_omx(self): self.new_matrix = AequilibraeMatrix() self.new_matrix.load(omx_example) arrays = ["m1", "m2"] self.new_matrix.computational_view(arrays) total_mats = np.sum(self.new_matrix.matrix_view) self.new_matrix.computational_view([arrays[0]]) total_m1 = np.sum(self.new_matrix.matrix_view) self.new_matrix.close() omx_file = omx.open_file(omx_example, "r") m1 = np.array(omx_file["m1"]).sum() m2 = np.array(omx_file["m2"]).sum() self.assertEqual(m1 + m2, total_mats) self.assertEqual(m1, total_m1) omx_file.close() del omx_file def test_copy(self): # test in-memory matrix_procedures copy matrix_copy = self.new_matrix.copy(self.copy_matrix_name, cores=["mat"]) if not np.array_equal(matrix_copy.mat, self.new_matrix.mat): self.fail("Matrix copy was not perfect") matrix_copy.close() del matrix_copy def test_export_to_csv(self): self.new_matrix.export(self.csv_export_name) df = pd.read_csv(self.csv_export_name) df.fillna(0, inplace=True) self.assertEqual(df.shape[0], 2500, "Exported wrong size") self.assertEqual(df.shape[1], 5, "Exported wrong size") self.assertAlmostEqual(df.mat.sum(), np.nansum(self.new_matrix.matrices), 5, "Exported wrong matrix total") def test_export_to_omx(self): self.new_matrix.export(self.omx_export_name) omxfile = omx.open_file(self.omx_export_name, "r") # Check if matrices values are compatible for m in self.new_matrix.names: sm = np.nansum(self.new_matrix.matrix[m]) sm2 = np.nansum(np.array(omxfile[m])) self.assertEqual(sm, sm2, "Matrix {} was exported with the wrong value".format(m)) del omxfile def test_nan_to_num(self): m = self.new_matrix.mat.sum() - self.new_matrix.mat[1, 1] self.new_matrix.computational_view(["mat", "seed"]) self.new_matrix.nan_to_num() self.new_matrix.mat[1, 1] = np.nan self.new_matrix.computational_view(["mat"]) self.new_matrix.nan_to_num() if abs(m - self.new_matrix.mat.sum()) > 0.000000000001: self.fail("Total for mat matrix not maintained") del self.new_matrix def test_copy_from_omx(self): temp_file = AequilibraeMatrix().random_name() a = AequilibraeMatrix() a.create_from_omx(temp_file, omx_example) omxfile = omx.open_file(omx_example, "r") # Check if matrices values are compatible for m in ["m1", "m2", "m3"]: sm = a.matrix[m].sum() sm2 = np.array(omxfile[m]).sum() if sm != sm2: self.fail("Matrix {} was copied with the wrong value".format(m)) if np.any(a.index[:] != np.array(list(omxfile.mapping("taz").keys()))): self.fail("Index was not created properly") a.close() del a del omxfile def test_copy_from_omx_long_name(self): temp_file = AequilibraeMatrix().random_name() a = AequilibraeMatrix() with self.assertRaises(ValueError): a.create_from_omx(temp_file, omx_example, robust=False) del a def test_copy_omx_wrong_content(self): # Check if we get a result if we try to copy non-existing cores temp_file = AequilibraeMatrix().random_name() a = AequilibraeMatrix() with self.assertRaises(ValueError): a.create_from_omx(temp_file, omx_example, cores=["m1", "m2", "m3", "m4"]) with self.assertRaises(ValueError): a.create_from_omx(temp_file, omx_example, mappings=["wrong index"]) del a def test_get_matrix(self): a = AequilibraeMatrix() a.load(self.sf_skims) with self.assertRaises(AttributeError): a.get_matrix("does not exist") q = a.get_matrix("distance") self.assertEqual(q.shape[0], 24) a = AequilibraeMatrix() a.load(self.name_test) print(np.array_equal(a.get_matrix("seed"), a.matrix["seed"])) del a def test_save(self): a = AequilibraeMatrix() a.load(self.sf_skims) a.computational_view(["distance"]) new_mat = np.random.rand(a.zones, a.zones) a.matrix_view *= new_mat res = a.matrix_view.sum() a.save("new_name_for_matrix") self.assertEqual(res, a.matrix_view.sum(), "Saved wrong result") a.save(["new_name_for_matrix2"]) self.assertEqual(a.view_names[0], "new_name_for_matrix2", "Did not update computational view") self.assertEqual(len(a.view_names), 1, "computational view with the wrong number of matrices") a.computational_view(["distance", "new_name_for_matrix"]) with self.assertRaises(ValueError): a.save(["just_one_name"]) a.save(["one_name", "two_names"]) with self.assertRaises(ValueError): a.save("distance") b = AequilibraeMatrix() b.load(self.name_test) b.computational_view("seed") b.save() b.computational_view(["mat", "seed", "dist"]) b.save()
class AssignmentResults: """ Assignment result holder for a single :obj:`TrafficClass` with multiple user classes """ def __init__(self): self.compact_link_loads = np.array([]) # Results for assignment on simplified graph self.compact_total_link_loads = np.array([]) # Results for all user classes summed on simplified graph self.link_loads = np.array([]) # The actual results for assignment self.total_link_loads = np.array([]) # The result of the assignment for all user classes summed self.crosswalk = np.array([]) # crosswalk between compact graph link IDs and actual link IDs self.skims = AequilibraeMatrix() # The array of skims self.no_path = None # The list os paths self.num_skims = 0 # number of skims that will be computed. Depends on the setting of the graph provided p = Parameters().parameters["system"]["cpus"] if not isinstance(p, int): p = 0 self.set_cores(p) self.classes = {"number": 1, "names": ["flow"]} self.nodes = -1 self.zones = -1 self.links = -1 self.compact_links = -1 self.compact_nodes = -1 self.__graph_id__ = None self.__float_type = None self.__integer_type = None self.lids = None self.direcs = None # In case we want to do by hand, we can prepare each method individually def prepare(self, graph: Graph, matrix: AequilibraeMatrix) -> None: """ Prepares the object with dimensions corresponding to the assignment matrix and graph objects Args: *graph* (:obj:`Graph`): Needs to have been set with number of centroids and list of skims (if any) *matrix* (:obj:`AequilibraeMatrix`): Matrix properly set for computation with matrix.computational_view(:obj:`list`) """ self.__float_type = graph.default_types("float") self.__integer_type = graph.default_types("int") if matrix.view_names is None: raise ("Please set the matrix_procedures computational view") else: self.classes["number"] = 1 if len(matrix.matrix_view.shape) > 2: self.classes["number"] = matrix.matrix_view.shape[2] self.classes["names"] = matrix.view_names if graph is None: raise ("Please provide a graph") else: self.compact_nodes = graph.compact_num_nodes self.compact_links = graph.compact_num_links self.nodes = graph.num_nodes self.zones = graph.num_zones self.centroids = graph.centroids self.links = graph.num_links self.num_skims = len(graph.skim_fields) self.skim_names = [x for x in graph.skim_fields] self.lids = graph.graph.link_id.values self.direcs = graph.graph.direction.values self.crosswalk = np.zeros(graph.graph.shape[0], self.__integer_type) self.crosswalk[graph.graph.__supernet_id__.values] = graph.graph.__compressed_id__.values self.__graph_ids = graph.graph.__supernet_id__.values self.__redim() self.__graph_id__ = graph.__id__ def reset(self) -> None: """ Resets object to prepared and pre-computation state """ if self.num_skims > 0: self.skims.matrices.fill(0) if self.link_loads is not None: self.no_path.fill(0) self.link_loads.fill(0) self.total_link_loads.fill(0) self.compact_link_loads.fill(0) self.compact_total_link_loads.fill(0) else: raise ValueError("Exception: Assignment results object was not yet prepared/initialized") def __redim(self): self.compact_link_loads = np.zeros((self.compact_links + 1, self.classes["number"]), self.__float_type) self.compact_total_link_loads = np.zeros(self.compact_links, self.__float_type) self.link_loads = np.zeros((self.links, self.classes["number"]), self.__float_type) self.total_link_loads = np.zeros(self.links, self.__float_type) self.no_path = np.zeros((self.zones, self.zones), dtype=self.__integer_type) if self.num_skims > 0: self.skims = AequilibraeMatrix() self.skims.create_empty(file_name=self.skims.random_name(), zones=self.zones, matrix_names=self.skim_names) self.skims.index[:] = self.centroids[:] self.skims.computational_view() if len(self.skims.matrix_view.shape[:]) == 2: self.skims.matrix_view = self.skims.matrix_view.reshape((self.zones, self.zones, 1)) else: self.skims = AequilibraeMatrix() self.skims.matrix_view = np.array((1, 1, 1)) self.reset() def total_flows(self) -> None: """ Totals all link flows for this class into a single link load Results are placed into *total_link_loads* class member """ sum_axis1(self.total_link_loads, self.link_loads, self.cores) 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 Args: *cores* (:obj:`int`): Number of cores to be used in computation """ if not isinstance(cores, int): raise ValueError("Number of cores needs to be an integer") if cores < 0: self.cores = max(1, mp.cpu_count() + cores) elif cores == 0: self.cores = mp.cpu_count() elif cores > 0: cores = min(mp.cpu_count(), cores) if self.cores != cores: self.cores = cores if self.link_loads.shape[0]: self.__redim() def get_load_results(self) -> AequilibraeData: """ Translates the assignment results from the graph format into the network format Returns: dataset (:obj:`AequilibraeData`): AequilibraE data with the traffic class assignment results """ fields = [] for n in self.classes["names"]: fields.extend([f"{n}_ab", f"{n}_ba", f"{n}_tot"]) types = [np.float64] * len(fields) entries = int(np.unique(self.lids).shape[0]) res = AequilibraeData() res.create_empty(memory_mode=True, entries=entries, field_names=fields, data_types=types) res.data.fill(np.nan) res.index[:] = np.unique(self.lids)[:] indexing = np.zeros(int(self.lids.max()) + 1, np.uint64) indexing[res.index[:]] = np.arange(entries) # Indices of links BA and AB ABs = self.direcs > 0 BAs = self.direcs < 0 ab_ids = indexing[self.lids[ABs]] ba_ids = indexing[self.lids[BAs]] # Link flows link_flows = self.link_loads[self.__graph_ids, :] for i, n in enumerate(self.classes["names"]): # AB Flows res.data[n + "_ab"][ab_ids] = np.nan_to_num(link_flows[ABs, i]) # BA Flows res.data[n + "_ba"][ba_ids] = np.nan_to_num(link_flows[BAs, i]) # Tot Flow res.data[n + "_tot"] = np.nan_to_num(res.data[n + "_ab"]) + np.nan_to_num(res.data[n + "_ba"]) return res def save_to_disk(self, file_name=None, output="loads") -> None: """ Function to write to disk all outputs computed during assignment Args: *file_name* (:obj:`str`): Name of the file, with extension. Valid extensions are: ['aed', 'csv', 'sqlite'] *output* (:obj:`str`, optional): Type of output ('loads', 'path_file'). Defaults to 'loads' """ if output == "loads": res = self.get_load_results() res.export(file_name) # TODO: Re-factor the exporting of the path file within the AequilibraeData format elif output == "path_file": raise NotImplementedError
column_vector = AequilibraEData() column_vector.create_empty(**args) column_vector.columns[:] = np.random.rand(zones)[:] * 1000 column_vector.index[:] = np.arange(zones)[:] # balance vectors column_vector.columns[:] = column_vector.columns[:] * ( row_vector.rows.sum() / column_vector.columns.sum()) # seed matrix_procedures name_test = AequilibraeMatrix().random_name() args = {"file_name": name_test, "zones": zones, "matrix_names": ["seed"]} matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.seed[:, :] = np.random.rand(zones, zones)[:, :] matrix.computational_view(["seed"]) matrix.matrix_view[1, 1] = np.nan matrix.index[:] = np.arange(zones)[:] class TestIpf(TestCase): def test_fit(self): # The IPF per se args = { "matrix": matrix, "rows": row_vector, "row_field": "rows", "columns": column_vector, "column_field": "columns", "nan_as_zero": False, }
# balance vectors column_vector.columns[:] = column_vector.columns[:] * ( row_vector.rows.sum() / column_vector.columns.sum()) # Impedance matrix_procedures name_test = os.path.join(tempfile.gettempdir(), 'aequilibrae_matrix_test.aem') args = {'file_name': name_test, 'zones': zones, 'matrix_names': ['impedance']} matrix = AequilibraeMatrix() matrix.create_empty(**args) # randoms = np.random.randint(5, size=(2, 4)) matrix.impedance[:, :] = np.random.rand(zones, zones)[:, :] matrix.index[:] = np.arange(matrix.zones) + 100 matrix.computational_view(['impedance']) model_expo = SyntheticGravityModel() model_expo.function = 'EXPO' model_expo.beta = 0.1 model_gamma = SyntheticGravityModel() model_gamma.function = 'GAMMA' model_gamma.beta = 0.1 model_gamma.alpha = -0.2 model_power = SyntheticGravityModel() model_power.function = 'POWER' model_power.alpha = -0.2
class TrafficAssignmentDialog(QtWidgets.QDialog, FORM_CLASS): def __init__(self, iface): QtWidgets.QDialog.__init__(self) self.iface = iface self.setupUi(self) self.path = standard_path() self.output_path = None self.temp_path = None self.error = None self.report = None self.method = {} self.matrices = OrderedDict() self.skims = [] self.matrix = None self.graph = Graph() self.results = AssignmentResults() self.block_centroid_flows = None self.worker_thread = None # Signals for the matrix_procedures tab self.but_load_new_matrix.clicked.connect(self.find_matrices) # Signals from the Network tab self.load_graph_from_file.clicked.connect(self.load_graph) # Signals for the algorithm tab self.progressbar0.setVisible(False) self.progressbar0.setValue(0) self.progress_label0.setVisible(False) self.do_assignment.clicked.connect(self.run) self.cancel_all.clicked.connect(self.exit_procedure) self.select_output_folder.clicked.connect(self.choose_folder_for_outputs) self.cb_choose_algorithm.addItem('All-Or-Nothing') self.cb_choose_algorithm.currentIndexChanged.connect(self.changing_algorithm) # slots for skim tab self.but_build_query.clicked.connect(partial(self.build_query, 'select link')) self.changing_algorithm() # path file self.path_file = OutputType() # Queries tables = [self.select_link_list, self.list_link_extraction] for table in tables: table.setColumnWidth(0, 280) table.setColumnWidth(1, 40) table.setColumnWidth(2, 150) table.setColumnWidth(3, 40) self.graph_properties_table.setColumnWidth(0, 190) self.graph_properties_table.setColumnWidth(1, 240) # critical link self.but_build_query.clicked.connect(partial(self.build_query, 'select link')) self.do_select_link.stateChanged.connect(self.set_behavior_special_analysis) self.tot_crit_link_queries = 0 self.critical_output = OutputType() # link flow extraction self.but_build_query_extract.clicked.connect(partial(self.build_query, 'Link flow extraction')) self.do_extract_link_flows.stateChanged.connect(self.set_behavior_special_analysis) self.tot_link_flow_extract = 0 self.link_extract = OutputType() # Disabling resources not yet implemented self.do_select_link.setEnabled(False) self.but_build_query.setEnabled(False) self.select_link_list.setEnabled(False) self.skim_list_table.setEnabled(False) self.do_extract_link_flows.setEnabled(False) self.but_build_query_extract.setEnabled(False) self.list_link_extraction.setEnabled(False) self.new_matrix_to_assign() self.table_matrix_list.setColumnWidth(0, 135) self.table_matrix_list.setColumnWidth(1, 135) self.table_matrices_to_assign.setColumnWidth(0, 125) self.table_matrices_to_assign.setColumnWidth(1, 125) self.skim_list_table.setColumnWidth(0, 70) self.skim_list_table.setColumnWidth(1, 490) def choose_folder_for_outputs(self): new_name = GetOutputFolderName(self.path, 'Output folder for traffic assignment') if new_name: self.output_path = new_name self.lbl_output.setText(new_name) else: self.output_path = None self.lbl_output.setText(new_name) def load_graph(self): self.lbl_graphfile.setText('') file_types = ["AequilibraE graph(*.aeg)"] default_type = '.aeg' box_name = 'Traffic Assignment' graph_file, _ = GetOutputFileName(self, box_name, file_types, default_type, self.path) if graph_file is not None: self.graph.load_from_disk(graph_file) fields = list(set(self.graph.graph.dtype.names) - set(self.graph.required_default_fields)) self.minimizing_field.addItems(fields) self.update_skim_list(fields) self.lbl_graphfile.setText(graph_file) cores = get_parameter_chain(['system', 'cpus']) self.results.set_cores(cores) # show graph properties def centers_item(qt_item): cell_widget = QWidget() lay_out = QHBoxLayout(cell_widget) lay_out.addWidget(qt_item) lay_out.setAlignment(Qt.AlignCenter) lay_out.setContentsMargins(0, 0, 0, 0) cell_widget.setLayout(lay_out) return cell_widget items = [['Graph ID', self.graph.__id__], ['Number of links', self.graph.num_links], ['Number of nodes', self.graph.num_nodes], ['Number of centroids', self.graph.num_zones]] self.graph_properties_table.clearContents() self.graph_properties_table.setRowCount(5) for i, item in enumerate(items): self.graph_properties_table.setItem(i, 0, QTableWidgetItem(item[0])) self.graph_properties_table.setItem(i, 1, QTableWidgetItem(str(item[1]))) self.graph_properties_table.setItem(4, 0, QTableWidgetItem('Block flows through centroids')) self.block_centroid_flows = QCheckBox() self.block_centroid_flows.setChecked(self.graph.block_centroid_flows) self.graph_properties_table.setCellWidget(4, 1, centers_item(self.block_centroid_flows)) else: self.graph = Graph() self.set_behavior_special_analysis() def changing_algorithm(self): if self.cb_choose_algorithm.currentText() == 'All-Or-Nothing': self.method['algorithm'] = 'AoN' def run_thread(self): self.worker_thread.assignment.connect(self.signal_handler) # QObject.connect(self.worker_thread, SIGNAL("assignment"), self.signal_handler) self.worker_thread.start() self.exec_() def job_finished_from_thread(self): self.report = self.worker_thread.report self.produce_all_outputs() self.exit_procedure() def run(self): if self.check_data(): self.set_output_names() self.progress_label0.setVisible(True) self.progressbar0.setVisible(True) self.progressbar0.setRange(0, self.graph.num_zones) try: if self.method['algorithm'] == 'AoN': self.worker_thread = allOrNothing(self.matrix, self.graph, self.results) self.run_thread() except ValueError as error: qgis.utils.iface.messageBar().pushMessage("Input error", error.message, level=3) else: qgis.utils.iface.messageBar().pushMessage("Input error", self.error, level=3) def set_output_names(self): self.path_file.temp_file = os.path.join(self.temp_path, 'path_file.aed') self.path_file.output_name = os.path.join(self.output_path, 'path_file') self.path_file.extension = 'aed' if self.do_path_file.isChecked(): self.results.setSavePathFile(save=True, path_result=self.path_file.temp_file) self.link_extract.temp_file = os.path.join(self.temp_path, 'link_extract') self.link_extract.output_name = os.path.join(self.output_path, 'link_extract') self.link_extract.extension = 'aed' self.critical_output.temp_file = os.path.join(self.temp_path, 'critical_output') self.critical_output.output_name = os.path.join(self.output_path, 'critical_output') self.critical_output.extension = 'aed' def check_data(self): self.error = None self.change_graph_settings() if not self.graph.num_links: self.error = 'Graph was not loaded' return False self.matrix = None if not self.prepare_assignable_matrices(): return False if self.matrix is None: self.error = 'Demand matrix missing' return False if self.output_path is None: self.error = 'Parameters for output missing' return False self.temp_path = os.path.join(self.output_path, 'temp') if not os.path.exists(self.temp_path): os.makedirs(self.temp_path) self.results.prepare(self.graph, self.matrix) return True def load_assignment_queries(self): # First we load the assignment queries query_labels = [] query_elements = [] query_types = [] if self.tot_crit_link_queries: for i in range(self.tot_crit_link_queries): links = eval(self.select_link_list.item(i, 0).text()) query_type = self.select_link_list.item(i, 1).text() query_name = self.select_link_list.item(i, 2).text() for l in links: d = directions_dictionary[l[1]] lk = self.graph.ids[(self.graph.graph['link_id'] == int(l[0])) & (self.graph.graph['direction'] == d)] query_labels.append(query_name) query_elements.append(lk) query_types.append(query_type) self.critical_queries = {'labels': query_labels, 'elements': query_elements, ' type': query_types} def signal_handler(self, val): if val[0] == 'zones finalized': self.progressbar0.setValue(val[1]) elif val[0] == 'text AoN': self.progress_label0.setText(val[1]) elif val[0] == 'finished_threaded_procedure': self.job_finished_from_thread() # TODO: Write code to export skims def produce_all_outputs(self): extension = 'aed' if not self.do_output_to_aequilibrae.isChecked(): extension = 'csv' if self.do_output_to_sqlite.isChecked(): extension = 'sqlite' # Save link flows to disk self.results.save_to_disk(os.path.join(self.output_path, 'link_flows.' + extension), output='loads') # save Path file if that is the case if self.do_path_file.isChecked(): if self.method['algorithm'] == 'AoN': if self.do_output_to_sqlite.isChecked(): self.results.save_to_disk(file_name=os.path.join(self.output_path, 'path_file.' + extension), output='path_file') # Saves output skims if self.skim_list_table.rowCount() > 0: self.results.skims.copy(os.path.join(self.output_path, 'skims.aem')) # if self.do_select_link.isChecked(): # if self.method['algorithm'] == 'AoN': # del(self.results.critical_links['results']) # self.results.critical_links = None # # shutil.move(self.critical_output.temp_file + '.aep', self.critical_output.output_name) # shutil.move(self.critical_output.temp_file + '.aed', self.critical_output.output_name[:-3] + 'aed') # # if self.do_extract_link_flows.isChecked(): # if self.method['algorithm'] == 'AoN': # del(self.results.link_extraction['results']) # self.results.link_extraction = None # # shutil.move(self.link_extract.temp_file + '.aep', self.link_extract.output_name) # shutil.move(self.link_extract.temp_file + '.aed', self.link_extract.output_name[:-3] + 'aed') # Procedures related to critical analysis. Not yet fully implemented def build_query(self, purpose): if purpose == 'select link': button = self.but_build_query message = 'Select Link Analysis' table = self.select_link_list counter = self.tot_crit_link_queries else: button = self.but_build_query_extract message = 'Link flow extraction' table = self.list_link_extraction counter = self.tot_link_flow_extract button.setEnabled(False) dlg2 = LoadSelectLinkQueryBuilderDialog(self.iface, self.graph.graph, message) dlg2.exec_() if dlg2.links is not None: table.setRowCount(counter + 1) text = '' for i in dlg2.links: text = text + ', (' + only_str(i[0]) + ', "' + only_str(i[1]) + '")' text = text[2:] table.setItem(counter, 0, QTableWidgetItem(text)) table.setItem(counter, 1, QTableWidgetItem(dlg2.query_type)) table.setItem(counter, 2, QTableWidgetItem(dlg2.query_name)) del_button = QPushButton('X') del_button.clicked.connect(partial(self.click_button_inside_the_list, purpose)) table.setCellWidget(counter, 3, del_button) counter += 1 if purpose == 'select link': self.tot_crit_link_queries = counter elif purpose == 'Link flow extraction': self.tot_link_flow_extract = counter button.setEnabled(True) def click_button_inside_the_list(self, purpose): if purpose == 'select link': table = self.select_link_list else: table = self.list_link_extraction button = self.sender() index = self.select_link_list.indexAt(button.pos()) row = index.row() table.removeRow(row) if purpose == 'select link': self.tot_crit_link_queries -= 1 elif purpose == 'Link flow extraction': self.tot_link_flow_extract -= 1 def set_behavior_special_analysis(self): if self.graph.num_links < 1: behavior = False else: behavior = True self.do_path_file.setEnabled(behavior) # This line of code turns off the features of select link analysis and link flow extraction while these # features are still being developed behavior = False self.do_select_link.setEnabled(behavior) self.do_extract_link_flows.setEnabled(behavior) self.but_build_query.setEnabled(behavior * self.do_select_link.isChecked()) self.select_link_list.setEnabled(behavior * self.do_select_link.isChecked()) self.list_link_extraction.setEnabled(behavior * self.do_extract_link_flows.isChecked()) self.but_build_query_extract.setEnabled(behavior * self.do_extract_link_flows.isChecked()) def update_skim_list(self, skims): self.skim_list_table.clearContents() self.skim_list_table.setRowCount(len(skims)) for i, skm in enumerate(skims): self.skim_list_table.setItem(i, 1, QTableWidgetItem(skm)) chb = QCheckBox() my_widget = QWidget() lay_out = QHBoxLayout(my_widget) lay_out.addWidget(chb) lay_out.setAlignment(Qt.AlignCenter) lay_out.setContentsMargins(0, 0, 0, 0) my_widget.setLayout(lay_out) self.skim_list_table.setCellWidget(i, 0, my_widget) # All Matrix loading and assignables selection def update_matrix_list(self): self.table_matrix_list.clearContents() self.table_matrix_list.clearContents() self.table_matrix_list.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table_matrix_list.setRowCount(len(self.matrices.keys())) for i, data_name in enumerate(self.matrices.keys()): self.table_matrix_list.setItem(i, 0, QTableWidgetItem(data_name)) cbox = QComboBox() for idx in self.matrices[data_name].index_names: cbox.addItem(str(idx)) self.table_matrix_list.setCellWidget(i, 1, cbox) def find_matrices(self): dlg2 = LoadMatrixDialog(self.iface) dlg2.show() dlg2.exec_() if dlg2.matrix is not None: matrix_name = dlg2.matrix.file_path matrix_name = os.path.splitext(os.path.basename(matrix_name))[0] matrix_name = self.find_non_conflicting_name(matrix_name, self.matrices) self.matrices[matrix_name] = dlg2.matrix self.update_matrix_list() row_count = self.table_matrices_to_assign.rowCount() new_matrix = list(self.matrices.keys())[-1] for i in range(row_count): cb = self.table_matrices_to_assign.cellWidget(i, 0) cb.insertItem(-1, new_matrix) def find_non_conflicting_name(self, data_name, dictio): if data_name in dictio: i = 1 new_data_name = data_name + '_' + str(i) while new_data_name in dictio: i += 1 new_data_name = data_name + '_' + str(i) data_name = new_data_name return data_name def changed_assignable_matrix(self, mi): chb = self.sender() mat_name = chb.currentText() table = self.table_matrices_to_assign for row in range(table.rowCount()): if table.cellWidget(row, 0) == chb: break if len(mat_name) == 0: if row + 1 < table.rowCount(): self.table_matrices_to_assign.removeRow(row) else: mat_cores = self.matrices[mat_name].names cbox2 = QComboBox() cbox2.addItems(mat_cores) self.table_matrices_to_assign.setCellWidget(row, 1, cbox2) if row + 1 == table.rowCount(): self.new_matrix_to_assign() def new_matrix_to_assign(self): # We edit ALL the combo boxes to have the current list of matrices row_count = self.table_matrices_to_assign.rowCount() self.table_matrices_to_assign.setRowCount(row_count + 1) cbox = QComboBox() cbox.addItems(list(self.matrices.keys())) cbox.addItem('') cbox.setCurrentIndex(cbox.count() - 1) cbox.currentIndexChanged.connect(self.changed_assignable_matrix) self.table_matrices_to_assign.setCellWidget(row_count, 0, cbox) def prepare_assignable_matrices(self): table = self.table_matrices_to_assign idx = self.graph.centroids mat_names = [] if table.rowCount() > 1: for row in range(table.rowCount() - 1): mat = table.cellWidget(row, 0).currentText() core = table.cellWidget(row, 1).currentText() mat_index = self.matrices[mat].index if not np.array_equal(idx, mat_index): no_zones = [item for item in mat_index if item not in idx] # We only return an error if the matrix has too many centroids if no_zones: self.error = 'Assignable matrix has centroids that do not exist in the network: {}'.format( ','.join([str(x) for x in no_zones])) return False if core in mat_names: self.error = 'Assignable matrices cannot have same names' return False mat_names.append(only_str(core)) self.matrix = AequilibraeMatrix() self.matrix.create_empty(file_name=self.matrix.random_name(), zones=idx.shape[0], matrix_names=mat_names) self.matrix.index[:] = idx[:] for row in range(table.rowCount() - 1): mat = table.cellWidget(row, 0).currentText() core = table.cellWidget(row, 1).currentText() src_mat = self.matrices[mat].matrix[core] dest_mat = self.matrix.matrix[core] rows = src_mat.shape[0] cols = src_mat.shape[1] dest_mat[:rows, :cols] = src_mat[:, :] # Inserts cols and rows that don;t exist if rows != self.matrix.zones: src_index = list(self.matrices[mat].index[:]) for i, row in enumerate(idx): if row not in src_index: dest_mat[i + 1:, :] = dest_mat[i:-1, :] dest_mat[i, :] = 0 if cols != self.matrix.zones: for j, col in enumerate(idx): if col not in src_index: dest_mat[:, j + 1:] = dest_mat[:, j:-1] dest_mat[:, j] = 0 self.matrix.computational_view() else: self.error = 'You need to have at least one matrix to assign' return False return True def change_graph_settings(self): skims = [] table = self.skim_list_table for i in range(table.rowCount()): for chb in table.cellWidget(i, 0).findChildren(QCheckBox): if chb.isChecked(): skims.append(only_str(table.item(i, 1).text())) if len(skims) == 0: skims = False self.graph.set_graph(cost_field=self.minimizing_field.currentText(), skim_fields=skims, block_centroid_flows=self.block_centroid_flows.isChecked()) def exit_procedure(self): self.close() if self.report: dlg2 = ReportDialog(self.iface, self.report) dlg2.show() dlg2.exec_()
# balance vectors column_vector.columns[:] = column_vector.columns[:] * (row_vector.rows.sum() / column_vector.columns.sum()) # Impedance matrix_procedures name_test = os.path.join(tempfile.gettempdir(), "aequilibrae_matrix_test.aem") args = {"file_name": name_test, "zones": zones, "matrix_names": ["impedance"]} matrix = AequilibraeMatrix() matrix.create_empty(**args) # randoms = np.random.randint(5, size=(2, 4)) matrix.impedance[:, :] = np.random.rand(zones, zones)[:, :] matrix.index[:] = np.arange(matrix.zones) + 100 matrix.computational_view(["impedance"]) model_expo = SyntheticGravityModel() model_expo.function = "EXPO" model_expo.beta = 0.1 model_gamma = SyntheticGravityModel() model_gamma.function = "GAMMA" model_gamma.beta = 0.1 model_gamma.alpha = -0.2 model_power = SyntheticGravityModel() model_power.function = "POWER" model_power.alpha = -0.2
class TestAequilibraeMatrix(TestCase): def test___init__(self): os.remove(name_test) if os.path.exists(name_test) else None args = { "file_name": name_test, "zones": zones, "matrix_names": ["mat", "seed", "dist"], "index_names": ["my indices"], } matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.index[:] = np.arange(matrix.zones) + 100 matrix.mat[:, :] = np.random.rand(matrix.zones, matrix.zones)[:, :] matrix.mat[:, :] = matrix.mat[:, :] * (1000 / np.sum(matrix.mat[:, :])) matrix.setName("Test matrix - " + str(random.randint(1, 10))) matrix.setDescription( "Generated at " + datetime.datetime.now().strftime("%I:%M%p on %B %d, %Y")) matrix.close() del matrix def test_load(self): # self.test___init__() self.new_matrix = AequilibraeMatrix() # Cannot load OMX file with no indices with self.assertRaises(LookupError): self.new_matrix.load(no_index_omx) self.new_matrix = AequilibraeMatrix() self.new_matrix.load(name_test) def test_computational_view(self): self.test_load() self.new_matrix.computational_view(["mat", "seed"]) self.new_matrix.mat.fill(0) self.new_matrix.seed.fill(0) if self.new_matrix.matrix_view.shape[2] != 2: self.fail( "Computational view returns the wrong number of matrices") self.new_matrix.computational_view(["mat"]) self.new_matrix.matrix_view[:, :] = np.arange(zones**2).reshape( zones, zones) if np.sum(self.new_matrix.mat) != np.sum(self.new_matrix.matrix_view): self.fail("Assigning to matrix view did not work") self.new_matrix.setName("Test matrix - " + str(random.randint(1, 10))) self.new_matrix.setDescription( "Generated at " + datetime.datetime.now().strftime("%I:%M%p on %B %d, %Y")) self.new_matrix.close() def test_computational_view_with_omx(self): self.new_matrix = AequilibraeMatrix() self.new_matrix.load(omx_example) arrays = ["m1", "m2"] self.new_matrix.computational_view(arrays) total_mats = np.sum(self.new_matrix.matrix_view) self.new_matrix.computational_view([arrays[0]]) total_m1 = np.sum(self.new_matrix.matrix_view) self.new_matrix.close() omx_file = omx.open_file(omx_example, "r") m1 = np.array(omx_file["m1"]).sum() m2 = np.array(omx_file["m2"]).sum() self.assertEqual(m1 + m2, total_mats) self.assertEqual(m1, total_m1) omx_file.close() def test_copy(self): self.test_load() # test in-memory matrix_procedures copy matrix_copy = self.new_matrix.copy(copy_matrix_name, cores=["mat"]) if not np.array_equal(matrix_copy.mat, self.new_matrix.mat): self.fail("Matrix copy was not perfect") matrix_copy.close() self.new_matrix.close() def test_export_to_csv(self): self.test_load() self.new_matrix.export(csv_export_name) self.new_matrix.close() def test_export_to_omx(self): self.test_load() self.new_matrix.export(omx_export_name) omxfile = omx.open_file(omx_export_name, "r") # Check if matrices values are compatible for m in self.new_matrix.names: sm = np.nansum(self.new_matrix.matrix[m]) sm2 = np.nansum(np.array(omxfile[m])) self.assertEqual( sm, sm2, "Matrix {} was exported with the wrong value".format(m)) self.new_matrix.close() def test_nan_to_num(self): self.test_load() s = self.new_matrix.seed.sum() - self.new_matrix.seed[1, 1] m = self.new_matrix.mat.sum() - self.new_matrix.mat[1, 1] self.new_matrix.seed[1, 1] = np.nan self.new_matrix.computational_view(["mat", "seed"]) self.new_matrix.nan_to_num() self.new_matrix.mat[1, 1] = np.nan self.new_matrix.computational_view(["mat"]) self.new_matrix.nan_to_num() if s != self.new_matrix.seed.sum(): self.fail("Total for seed matrix not maintained") if m != self.new_matrix.mat.sum(): self.fail("Total for mat matrix not maintained") def test_copy_from_omx(self): temp_file = AequilibraeMatrix().random_name() a = AequilibraeMatrix() a.create_from_omx(temp_file, omx_example) omxfile = omx.open_file(omx_example, "r") # Check if matrices values are compatible for m in ["m1", "m2", "m3"]: sm = a.matrix[m].sum() sm2 = np.array(omxfile[m]).sum() if sm != sm2: self.fail( "Matrix {} was copied with the wrong value".format(m)) if np.any(a.index[:] != np.array(list(omxfile.mapping("taz").keys()))): self.fail("Index was not created properly") a.close() def test_copy_from_omx_long_name(self): temp_file = AequilibraeMatrix().random_name() a = AequilibraeMatrix() with self.assertRaises(ValueError): a.create_from_omx(temp_file, omx_example, robust=False) def test_copy_omx_wrong_content(self): # Check if we get a result if we try to copy non-existing cores temp_file = AequilibraeMatrix().random_name() a = AequilibraeMatrix() with self.assertRaises(ValueError): a.create_from_omx(temp_file, omx_example, cores=["m1", "m2", "m3", "m4"]) with self.assertRaises(ValueError): a.create_from_omx(temp_file, omx_example, mappings=["wrong index"])
class DisplayAequilibraEFormatsDialog(QtWidgets.QDialog, FORM_CLASS): def __init__(self, iface): QtWidgets.QDialog.__init__(self) self.iface = iface self.setupUi(self) self.error = None self.error = None self.data_path, self.data_type = GetOutputFileName( self, 'AequilibraE custom formats', ["Aequilibrae dataset(*.aed)", "Aequilibrae matrix(*.aem)"], '.aed', standard_path()) if self.data_type is None: self.error = 'Path provided is not a valid dataset' self.exit_with_error() self.data_type = self.data_type.upper() if self.data_type == 'AED': self.data_to_show = AequilibraEData() elif self.data_type == 'AEM': self.data_to_show = AequilibraeMatrix() try: self.data_to_show.load(self.data_path) except: self.error = 'Could not load dataset' self.exit_with_error() # Elements that will be used during the displaying self._layout = QVBoxLayout() self.table = QTableView() self._layout.addWidget(self.table) # Settings for displaying self.show_layout = QHBoxLayout() # Thousand separator self.thousand_separator = QCheckBox() self.thousand_separator.setChecked(True) self.thousand_separator.setText('Thousands separator') self.thousand_separator.toggled.connect(self.format_showing) self.show_layout.addWidget(self.thousand_separator) self.spacer = QSpacerItem(5, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.show_layout.addItem(self.spacer) # Decimals txt = QLabel() txt.setText('Decimal places') self.show_layout.addWidget(txt) self.decimals = QSpinBox() self.decimals.valueChanged.connect(self.format_showing) self.decimals.setMinimum(0) self.decimals.setValue(4) self.decimals.setMaximum(10) self.show_layout.addWidget(self.decimals) self._layout.addItem(self.show_layout) # differentiates between matrix and dataset if self.data_type == 'AEM': self.data_to_show.computational_view([self.data_to_show.names[0]]) # Matrices need cores and indices to be set as well self.mat_layout = QHBoxLayout() self.mat_list = QComboBox() for n in self.data_to_show.names: self.mat_list.addItem(n) self.mat_list.currentIndexChanged.connect(self.change_matrix_cores) self.mat_layout.addWidget(self.mat_list) self.idx_list = QComboBox() for i in self.data_to_show.index_names: self.idx_list.addItem(i) self.idx_list.currentIndexChanged.connect(self.change_matrix_cores) self.mat_layout.addWidget(self.idx_list) self._layout.addItem(self.mat_layout) self.change_matrix_cores() self.but_export = QPushButton() self.but_export.setText('Export') self.but_export.clicked.connect(self.export) self.but_close = QPushButton() self.but_close.clicked.connect(self.exit_procedure) self.but_close.setText('Close') self.but_layout = QHBoxLayout() self.but_layout.addWidget(self.but_export) self.but_layout.addWidget(self.but_close) self._layout.addItem(self.but_layout) # We chose to use QTableView. However, if we want to allow the user to edit the dataset # The we need to allow them to switch to the slower QTableWidget # Code below # self.table = QTableWidget(self.data_to_show.entries, self.data_to_show.num_fields) # self.table.setHorizontalHeaderLabels(self.data_to_show.fields) # self.table.setObjectName('data_viewer') # # self.table.setVerticalHeaderLabels([str(x) for x in self.data_to_show.index[:]]) # self.table.clearContents() # # for i in range(self.data_to_show.entries): # for j, f in enumerate(self.data_to_show.fields): # item1 = QTableWidgetItem(str(self.data_to_show.data[f][i])) # item1.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) # self.table.setItem(i, j, item1) self.resize(700, 500) self.setLayout(self._layout) self.format_showing() def format_showing(self): decimals = self.decimals.value() separator = self.thousand_separator.isChecked() if isinstance(self.data_to_show, AequilibraeMatrix): m = NumpyModel(self.data_to_show, separator, decimals) else: m = DatabaseModel(self.data_to_show, separator, decimals) self.table.clearSpans() self.table.setModel(m) def change_matrix_cores(self): self.data_to_show.computational_view([self.mat_list.currentText()]) self.data_to_show.set_index(self.data_to_show.index_names[0]) self.format_showing() def export(self): new_name, file_type = GetOutputFileName( self, self.data_type, ["Comma-separated file(*.csv)"], ".csv", self.data_path) if new_name is not None: self.data_to_show.export(new_name) def exit_with_error(self): qgis.utils.iface.messageBar().pushMessage("Error:", self.error, level=1) self.exit_procedure() def exit_procedure(self): self.close()
class TestTrafficAssignment(TestCase): def setUp(self) -> None: self.matrix = AequilibraeMatrix() self.matrix.load(siouxfalls_demand) self.matrix.computational_view() self.project = Project() self.project.load(siouxfalls_project) 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.assignment = TrafficAssignment() self.assigclass = TrafficClass(self.car_graph, self.matrix) def tearDown(self) -> None: self.matrix.close() self.project.conn.close() def test_set_vdf(self): with self.assertRaises(ValueError): self.assignment.set_vdf('CQS') self.assignment.set_vdf('BPR') def test_set_classes(self): with self.assertRaises(ValueError): self.assignment.set_classes([1, 2]) # The traffic assignment class is unprotected. # Should we protect it? # self.assigclass = TrafficClass(self.car_graph, self.matrix) # self.assigclass.graph = 1 # with self.assertRaises(ValueError): # self.assignment.set_classes(self.assigclass) self.assignment.set_classes(self.assigclass) # self.fail() def test_algorithms_available(self): algs = self.assignment.algorithms_available() real = ['all-or-nothing', 'msa', 'frank-wolfe', 'bfw', 'cfw'] 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') def test_set_cores(self): with self.assertRaises(Exception): self.assignment.set_cores(3) self.assignment.set_classes(self.assigclass) with self.assertRaises(ValueError): self.assignment.set_cores('q') self.assignment.set_cores(3) def test_set_algorithm(self): with self.assertRaises(AttributeError): self.assignment.set_algorithm('not an algo') self.assignment.set_classes(self.assigclass) with self.assertRaises(Exception): self.assignment.set_algorithm('msa') self.assignment.set_vdf("BPR") 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 = 10 self.assignment.set_algorithm('bfw') def test_set_vdf_parameters(self): with self.assertRaises(Exception): self.assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) self.assignment.set_vdf('bpr') self.assignment.set_classes(self.assigclass) self.assignment.set_vdf_parameters({"alpha": "b", "beta": "power"}) def test_set_time_field(self): N = random.randint(1, 50) val = ''.join( random.choices(string.ascii_uppercase + string.digits, k=N)) self.assignment.set_time_field(val) self.assertEqual(self.assignment.time_field, val) def test_set_capacity_field(self): N = random.randint(1, 50) val = ''.join( random.choices(string.ascii_uppercase + string.digits, k=N)) self.assignment.set_capacity_field(val) self.assertEqual(self.assignment.capacity_field, val) def test_execute(self): self.assignment.set_classes(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 = 10 self.assignment.set_algorithm('msa') self.assignment.execute() msa10 = self.assignment.assignment.rgap self.assigclass.results.total_flows() correl = np.corrcoef(self.assigclass.results.total_link_loads, self.assigclass.graph.graph['volume'])[0, 1] self.assertLess(0.8, correl) self.assignment.max_iter = 30 self.assignment.set_algorithm('msa') self.assignment.execute() msa25 = self.assignment.assignment.rgap self.assigclass.results.total_flows() correl = np.corrcoef(self.assigclass.results.total_link_loads, self.assigclass.graph.graph['volume'])[0, 1] self.assertLess(0.95, correl) self.assignment.set_algorithm('frank-wolfe') self.assignment.execute() fw25 = self.assignment.assignment.rgap self.assigclass.results.total_flows() correl = np.corrcoef(self.assigclass.results.total_link_loads, self.assigclass.graph.graph['volume'])[0, 1] self.assertLess(0.97, correl) self.assignment.set_algorithm('cfw') self.assignment.execute() cfw25 = self.assignment.assignment.rgap self.assigclass.results.total_flows() correl = np.corrcoef(self.assigclass.results.total_link_loads, self.assigclass.graph.graph['volume'])[0, 1] self.assertLess(0.98, correl) self.assignment.set_algorithm('bfw') self.assignment.execute() bfw25 = self.assignment.assignment.rgap self.assigclass.results.total_flows() correl = np.corrcoef(self.assigclass.results.total_link_loads, self.assigclass.graph.graph['volume'])[0, 1] self.assertLess(0.99, correl) self.assertLess(msa25, msa10) self.assertLess(fw25, msa25) self.assertLess(cfw25, fw25) self.assertLess(bfw25, cfw25)
column_vector = AequilibraEData() column_vector.create_empty(**args) column_vector.columns[:] = np.random.rand(zones)[:] * 1000 column_vector.index[:] = np.arange(zones)[:] # balance vectors column_vector.columns[:] = column_vector.columns[:] * ( row_vector.rows.sum() / column_vector.columns.sum()) # seed matrix_procedures name_test = AequilibraeMatrix().random_name() args = {'file_name': name_test, 'zones': zones, 'matrix_names': ['seed']} matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.seed[:, :] = np.random.rand(zones, zones)[:, :] matrix.computational_view(['seed']) matrix.matrix_view[1, 1] = np.nan matrix.index[:] = np.arange(zones)[:] class TestIpf(TestCase): def test_fit(self): # The IPF per se args = { 'matrix': matrix, 'rows': row_vector, 'row_field': 'rows', 'columns': column_vector, 'column_field': 'columns', 'nan_as_zero': False }
import os, tempfile zones = 100 # Impedance matrix_procedures name_test = AequilibraeMatrix().random_name() args = {'file_name': name_test, 'zones': zones, 'matrix_names': ['impedance']} impedance = AequilibraeMatrix() impedance.create_empty(**args) impedance.impedance[:, :] = np.random.rand(zones, zones)[:,:] * 1000 impedance.index[:] = np.arange(impedance.zones) + 100 impedance.computational_view(['impedance']) args['matrix_names'] = ['base_matrix'] args['file_name'] = AequilibraeMatrix().random_name() matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.base_matrix[:, :] = np.random.rand(zones, zones)[:,:] * 1000 matrix.index[:] = np.arange(matrix.zones) + 100 matrix.computational_view(['base_matrix']) class TestGravityCalibration(TestCase): def test_calibrate(self): args = {'impedance': impedance, 'matrix': matrix,
from aequilibrae.distribution import GravityCalibration from aequilibrae.matrix import AequilibraeMatrix from ...data import siouxfalls_demand, siouxfalls_skims zones = 100 # Impedance matrix_procedures name_test = AequilibraeMatrix().random_name() args = {"file_name": name_test, "zones": zones, "matrix_names": ["impedance"]} impedance = AequilibraeMatrix() impedance.create_empty(**args) impedance.impedance[:, :] = np.random.rand(zones, zones)[:, :] * 1000 impedance.index[:] = np.arange(impedance.zones) + 100 impedance.computational_view(["impedance"]) args["matrix_names"] = ["base_matrix"] args["file_name"] = AequilibraeMatrix().random_name() matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.base_matrix[:, :] = np.random.rand(zones, zones)[:, :] * 1000 matrix.index[:] = np.arange(matrix.zones) + 100 matrix.computational_view(["base_matrix"]) class TestGravityCalibration(TestCase): def test_calibrate(self): args = {"impedance": impedance, "matrix": matrix, "function": "power", "nan_to_zero": False} distributed_matrix = GravityCalibration(**args)
column_vector.columns[:] = column_vector.columns[:] * (row_vector.rows.sum() / column_vector.columns.sum()) # Impedance matrix_procedures name_test = os.path.join(tempfile.gettempdir(), 'aequilibrae_matrix_test.aem') args = {'file_name': name_test, 'zones': zones, 'matrix_names': ['impedance']} matrix = AequilibraeMatrix() matrix.create_empty(**args) # randoms = np.random.randint(5, size=(2, 4)) matrix.impedance[:, :] = np.random.rand(zones, zones)[:, :] matrix.index[:] = np.arange(matrix.zones) + 100 matrix.computational_view(['impedance']) model_expo = SyntheticGravityModel() model_expo.function = 'EXPO' model_expo.beta = 0.1 model_gamma = SyntheticGravityModel() model_gamma.function = 'GAMMA' model_gamma.beta = 0.1 model_gamma.alpha = -0.2 model_power = SyntheticGravityModel() model_power.function = 'POWER' model_power.alpha = -0.2
class AssignmentResults: """ Assignment result holder for a single :obj:`TrafficClass` with multiple user classes """ def __init__(self): self.link_loads = None # type: np.array # The actual results for assignment self.total_link_loads = None # type: np.array # The result of the assignment for all user classes summed self.skims = None # The array of skims self.no_path = None # The list os paths self.num_skims = None # number of skims that will be computed. Depends on the setting of the graph provided p = Parameters().parameters['system']['cpus'] if not isinstance(p, int): p = 0 self.set_cores(p) self.classes = {"number": 1, "names": ["flow"]} self.critical_links = { "save": False, "queries": {}, "results": False } # Queries are a dictionary self.link_extraction = { "save": False, "queries": {}, "output": None } # Queries are a dictionary self.path_file = {"save": False, "results": None} self.nodes = -1 self.zones = -1 self.links = -1 self.__graph_id__ = None self.__float_type = None self.__integer_type = None self.lids = None self.direcs = None # In case we want to do by hand, we can prepare each method individually def prepare(self, graph: Graph, matrix: AequilibraeMatrix) -> None: """ Prepares the object with dimensions corresponding to the assignment matrix and graph objects Args: *graph* (:obj:`Graph`): Needs to have been set with number of centroids and list of skims (if any) *matrix* (:obj:`AequilibraeMatrix`): Matrix properly set for computation with matrix.computational_view(:obj:`list`) """ self.__float_type = graph.default_types("float") self.__integer_type = graph.default_types("int") if matrix.view_names is None: raise ("Please set the matrix_procedures computational view") else: self.classes["number"] = 1 if len(matrix.matrix_view.shape) > 2: self.classes["number"] = matrix.matrix_view.shape[2] self.classes["names"] = matrix.view_names if graph is None: raise ("Please provide a graph") else: self.nodes = graph.num_nodes self.zones = graph.num_zones self.centroids = graph.centroids self.links = graph.num_links self.num_skims = len(graph.skim_fields) self.skim_names = [x for x in graph.skim_fields] self.lids = graph.graph["link_id"] self.direcs = graph.graph["direction"] self.__redim() self.__graph_id__ = graph.__id__ # TODO: Enable these methods when the work for select link analysis and saving path files is completed self.__setSavePathFile(False) self.__setCriticalLinks(False) def reset(self) -> None: """ Resets object to prepared and pre-computation state """ if self.num_skims > 0: self.skims.matrices.fill(0) if self.link_loads is not None: self.no_path.fill(0) self.link_loads.fill(0) self.total_link_loads.fill(0) else: raise ValueError( "Exception: Assignment results object was not yet prepared/initialized" ) def __redim(self): self.link_loads = np.zeros((self.links, self.classes["number"]), self.__float_type) self.total_link_loads = np.zeros(self.links, self.__float_type) self.no_path = np.zeros((self.zones, self.zones), dtype=self.__integer_type) if self.num_skims > 0: self.skims = AequilibraeMatrix() self.skims.create_empty(file_name=self.skims.random_name(), zones=self.zones, matrix_names=self.skim_names) self.skims.index[:] = self.centroids[:] self.skims.computational_view() if len(self.skims.matrix_view.shape[:]) == 2: self.skims.matrix_view = self.skims.matrix_view.reshape( (self.zones, self.zones, 1)) else: self.skims = AequilibraeMatrix() self.skims.matrix_view = np.array((1, 1, 1)) self.reset() def total_flows(self) -> None: """ Totals all link flows for this class into a single link load Results are placed into *total_link_loads* class member """ sum_axis1(self.total_link_loads, self.link_loads, self.cores) 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 Args: *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 = max(mp.cpu_count(), cores) if self.cores != cores: self.cores = cores if self.link_loads is not None: self.__redim() else: raise ValueError("Number of cores needs to be an integer") def __setCriticalLinks(self, save=False, queries={}, crit_res_result=None): a = AequilibraeMatrix() if save: if crit_res_result is None: warnings.warn( "Critical Link analysis not set properly. Need to specify output file too" ) else: if crit_res_result[-3:].lower() != "aem": crit_res_result += ".aes" if self.nodes > 0 and self.zones > 0: if ["elements", "labels", "type"] in queries.keys(): if len(queries["labels"]) == len( queries["elements"]) == len(queries["type"]): a.create_empty(file_name=crit_res_result, zones=self.zones, matrix_names=queries["labels"]) else: raise ValueError( "Queries are inconsistent. 'Labels', 'elements' and 'type' need to have same dimensions" ) else: raise ValueError( "Queries are inconsistent. It needs to contain the following elements: 'Labels', 'elements' and 'type'" ) else: a.create_empty(file_name=a.random_name(), zones=self.zones, matrix_names=["empty", "nothing"]) a.computational_view() if len(a.matrix_view.shape[:]) == 2: a.matrix_view = a.matrix_view.reshape((self.zones, self.zones, 1)) self.critical_links = {"save": save, "queries": queries, "results": a} def __setSavePathFile(self, save=False, path_result=None): # Fields: Origin, Node, Predecessor # Number of records: Origins * Nodes a = AequilibraeData() d1 = max(1, self.zones) d2 = 1 memory_mode = True if save: if path_result is None: warnings.warn( "Path file not set properly. Need to specify output file too" ) else: # This is the only place where we keep 32bits, as going 64 bits would explode the file size if self.nodes > 0 and self.zones > 0: d1 = self.zones d2 = self.nodes memory_mode = False a.create_empty( file_path=path_result, entries=d1 * d2, field_names=["origin", "node", "predecessor", "connector"], data_types=[np.uint32, np.uint32, np.uint32, np.uint32], memory_mode=memory_mode, ) self.path_file = {"save": save, "results": a} def get_load_results(self) -> AequilibraeData: """ Translates the assignment results from the graph format into the network format Returns: dataset (:obj:`AequilibraeData`): AequilibraE data with the traffic class assignment results """ fields = ['link_id'] for n in self.classes['names']: fields.extend([f'{n}_ab', f'{n}_ba', f'{n}_tot']) types = [np.float64] * len(fields) entries = int(np.unique(self.lids).shape[0]) res = AequilibraeData() res.create_empty(memory_mode=True, entries=entries, field_names=fields, data_types=types) res.data.fill(np.nan) res.index[:] = np.unique(self.lids)[:] res.link_id[:] = res.index[:] indexing = np.zeros(int(self.lids.max()) + 1, np.uint64) indexing[res.index[:]] = np.arange(entries) # Indices of links BA and AB ABs = self.direcs > 0 BAs = self.direcs < 0 ab_ids = indexing[self.lids[ABs]] ba_ids = indexing[self.lids[BAs]] # Link flows link_flows = self.link_loads[:, :] for i, n in enumerate(self.classes["names"]): # AB Flows res.data[n + "_ab"][ab_ids] = np.nan_to_num(link_flows[ABs, i]) # BA Flows res.data[n + "_ba"][ba_ids] = np.nan_to_num(link_flows[BAs, i]) # Tot Flow res.data[n + "_tot"] = np.nan_to_num( res.data[n + "_ab"]) + np.nan_to_num(res.data[n + "_ba"]) return res def save_to_disk(self, file_name=None, output="loads") -> None: """ Function to write to disk all outputs computed during assignment Args: *file_name* (:obj:`str`): Name of the file, with extension. Valid extensions are: ['aed', 'csv', 'sqlite'] *output* (:obj:`str`, optional): Type of output ('loads', 'path_file'). Defaults to 'loads' """ if output == "loads": res = self.get_load_results() res.export(file_name) # TODO: Re-factor the exporting of the path file within the AequilibraeData format elif output == "path_file": pass
class Ipf: """Iterative proportional fitting procedure :: import pandas as pd from aequilibrae.distribution import Ipf from aequilibrae.matrix import AequilibraeMatrix from aequilibrae.matrix import AequilibraeData matrix = AequilibraeMatrix() # Here we can create from OMX or load from an AequilibraE matrix. matrix.create_from_omx(path/to/aequilibrae_matrix, path/to/omxfile) # The matrix will be operated one (see the note on overwriting), so it does # not make sense load an OMX matrix source_vectors = pd.read_csv(path/to/CSVs) zones = source_vectors.zone.shape[0] args = {"entries": zones, "field_names": ["productions", "attractions"], "data_types": [np.float64, np.float64], "memory_mode": True} vectors = AequilibraEData() vectors.create_empty(**args) vectors.productions[:] = source_vectors.productions[:] vectors.attractions[:] = source_vectors.attractions[:] # We assume that the indices would be sorted and that they would match the matrix indices vectors.index[:] = source_vectors.zones[:] args = { "matrix": matrix, "rows": vectors, "row_field": "productions", "columns": vectors, "column_field": "attractions", "nan_as_zero": False} fratar = Ipf(**args) fratar.fit() # We can get back to our OMX matrix in the end fratar.output.export(path/to_omx/output.omx) fratar.output.export(path/to_aem/output.aem) """ def __init__(self, **kwargs): """ Instantiates the Ipf problem Args: matrix (:obj:`AequilibraeMatrix`): Seed Matrix rows (:obj:`AequilibraeData`): Vector object with data for row totals row_field (:obj:`str`): Field name that contains the data for the row totals columns (:obj:`AequilibraeData`): Vector object with data for column totals column_field (:obj:`str`): Field name that contains the data for the column totals parameters (:obj:`str`, optional): Convergence parameters. Defaults to those in the parameter file nan_as_zero (:obj:`bool`, optional): If Nan values should be treated as zero. Defaults to True Results: output (:obj:`AequilibraeMatrix`): Result Matrix report (:obj:`list`): Iteration and convergence report error (:obj:`str`): Error description """ self.parameters = kwargs.get("parameters", self.__get_parameters("ipf")) # Seed matrix self.matrix = kwargs.get("matrix", None) # NaN as zero self.nan_as_zero = kwargs.get("nan_as_zero", True) # row vector self.rows = kwargs.get("rows", None) self.row_field = kwargs.get("row_field", None) self.output_name = kwargs.get("output", AequilibraeMatrix().random_name()) # Column vector self.columns = kwargs.get("columns", None) self.column_field = kwargs.get("column_field", None) self.output = AequilibraeMatrix() self.error = None self.__required_parameters = [ "convergence level", "max iterations", "balancing tolerance" ] self.error_free = True self.report = [" ##### IPF computation ##### ", ""] self.gap = None self.procedure_date = '' self.procedure_id = '' def __check_data(self): self.error = None self.__check_parameters() # check data types if not isinstance(self.rows, AequilibraeData): raise TypeError( "Row vector needs to be an instance of AequilibraeData") if not isinstance(self.columns, AequilibraeData): raise TypeError( "Column vector needs to be an instance of AequilibraeData") if not isinstance(self.matrix, AequilibraeMatrix): raise TypeError( "Seed matrix needs to be an instance of AequilibraeMatrix") # Check data type if not np.issubdtype(self.matrix.dtype, np.floating): raise ValueError("Seed matrix need to be a float type") row_data = self.rows.data col_data = self.columns.data if not np.issubdtype(row_data[self.row_field].dtype, np.floating): raise ValueError("production/rows vector must be a float type") if not np.issubdtype(col_data[self.column_field].dtype, np.floating): raise ValueError("Attraction/columns vector must be a float type") # Check data dimensions if not np.array_equal(self.rows.index, self.columns.index): raise ValueError( "Indices from row vector do not match those from column vector" ) if not np.array_equal(self.matrix.index, self.columns.index): raise ValueError( "Indices from vectors do not match those from seed matrix") # Check if matrix was set for computation if self.matrix.matrix_view is None: raise ValueError("Matrix needs to be set for computation") else: if len(self.matrix.matrix_view.shape[:]) > 2: raise ValueError( "Matrix' computational view needs to be set for a single matrix core" ) if self.error is None: # check balancing: sum_rows = np.nansum(row_data[self.row_field]) sum_cols = np.nansum(col_data[self.column_field]) if abs(sum_rows - sum_cols) > self.parameters["balancing tolerance"]: self.error = "Vectors are not balanced" else: # guarantees that they are precisely balanced col_data[self.column_field][:] = col_data[ self.column_field][:] * (sum_rows / sum_cols) if self.error is not None: self.error_free = False def __check_parameters(self): for i in self.__required_parameters: if i not in self.parameters: self.error = "Parameters error. It needs to be a dictionary with the following keys: " for t in self.__required_parameters: self.error = self.error + t + ", " if self.error: raise ValueError(self.error) def fit(self): """Runs the IPF instance problem to adjust the matrix Resulting matrix is the *output* class member """ self.procedure_id = uuid4().hex self.procedure_date = str(datetime.today()) t = perf_counter() self.__check_data() if self.error_free: max_iter = self.parameters["max iterations"] conv_criteria = self.parameters["convergence level"] if self.matrix.is_omx(): self.output = AequilibraeMatrix() self.output.create_from_omx(self.output.random_name(), self.matrix.file_path, cores=self.matrix.view_names) self.output.computational_view() else: self.output = self.matrix.copy(self.output_name) if self.nan_as_zero: self.output.matrix_view[:, :] = np.nan_to_num( self.output.matrix_view)[:, :] rows = self.rows.data[self.row_field] columns = self.columns.data[self.column_field] tot_matrix = np.nansum(self.output.matrix_view[:, :]) # Reporting self.report.append("Target convergence criteria: " + str(conv_criteria)) self.report.append("Maximum iterations: " + str(max_iter)) self.report.append("") self.report.append("Rows:" + str(self.rows.entries)) self.report.append("Columns: " + str(self.columns.entries)) self.report.append("Total of seed matrix: " + "{:28,.4f}".format(float(tot_matrix))) self.report.append("Total of target vectors: " + "{:25,.4f}".format(float(np.nansum(rows)))) self.report.append("") self.report.append("Iteration, Convergence") self.gap = conv_criteria + 1 iter = 0 while self.gap > conv_criteria and iter < max_iter: iter += 1 # computes factors for zones marg_rows = self.__tot_rows(self.output.matrix_view[:, :]) row_factor = self.__factor(marg_rows, rows) # applies factor self.output.matrix_view[:, :] = np.transpose( np.transpose(self.output.matrix_view[:, :]) * np.transpose(row_factor))[:, :] # computes factors for columns marg_cols = self.__tot_columns(self.output.matrix_view[:, :]) column_factor = self.__factor(marg_cols, columns) # applies factor self.output.matrix_view[:, :] = self.output.matrix_view[:, :] * column_factor # increments iterarions and computes errors self.gap = max( abs(1 - np.min(row_factor)), abs(np.max(row_factor) - 1), abs(1 - np.min(column_factor)), abs(np.max(column_factor) - 1), ) self.report.append( str(iter) + " , " + str("{:4,.10f}".format(float(np.nansum(self.gap))))) self.report.append("") self.report.append("Running time: " + str("{:4,.3f}".format(perf_counter() - t)) + "s") def save_to_project(self, name: str, file_name: str) -> MatrixRecord: """Saves the matrix output to the project file Args: name (:obj:`str`): Name of the desired matrix record file_name (:obj:`str`): Name for the matrix file name. AEM and OMX supported """ mats = Matrices() record = mats.new_record(name, file_name, self.output) record.procedure_id = self.procedure_id record.timestamp = self.procedure_date record.procedure = 'Iterative Proportional fitting' record.save() return record def __tot_rows(self, matrix): return np.nansum(matrix, axis=1) def __tot_columns(self, matrix): return np.nansum(matrix, axis=0) def __factor(self, marginals, targets): f = np.divide(targets, marginals) # We compute the factors f[f == np.NINF] = 1 # And treat the errors return f def __get_parameters(self, model): path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) with open(path + "/parameters.yml", "r") as yml: path = yaml.safe_load(yml) return path["distribution"][model]
class TestAequilibraeMatrix(TestCase): def test___init__(self): os.remove(name_test) if os.path.exists(name_test) else None args = {'file_name': name_test, 'zones': zones, 'matrix_names': ['mat', 'seed', 'dist'], 'index_names': ['my indices']} matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.index[:] = np.arange(matrix.zones) + 100 matrix.mat[:, :] = np.random.rand(matrix.zones, matrix.zones)[:, :] matrix.mat[:, :] = matrix.mat[:, :] * (1000 / np.sum(matrix.mat[:, :])) matrix.setName('Test matrix - ' + str(random.randint(1, 10))) matrix.setDescription('Generated at ' + datetime.datetime.now().strftime("%I:%M%p on %B %d, %Y")) matrix.close(True) del (matrix) def test_load(self): # self.test___init__() self.new_matrix = AequilibraeMatrix() self.new_matrix.load(name_test) def test_computational_view(self): self.test_load() self.new_matrix.computational_view(['mat', 'seed']) self.new_matrix.mat.fill(0) self.new_matrix.seed.fill(0) if self.new_matrix.matrix_view.shape[2] != 2: self.fail('Computational view returns the wrong number of matrices') self.new_matrix.computational_view(['mat']) self.new_matrix.matrix_view[:, :] = np.arange(zones ** 2).reshape(zones, zones) if np.sum(self.new_matrix.mat) != np.sum(self.new_matrix.matrix_view): self.fail('Assigning to matrix view did not work') self.new_matrix.setName('Test matrix - ' + str(random.randint(1, 10))) self.new_matrix.setDescription('Generated at ' + datetime.datetime.now().strftime("%I:%M%p on %B %d, %Y")) self.new_matrix.close(True) def test_copy(self): self.test_load() # test in-memory matrix_procedures copy matrix_copy = self.new_matrix.copy(copy_matrix_name, cores=['mat']) if not np.array_equal(matrix_copy.mat, self.new_matrix.mat): self.fail('Matrix copy was not perfect') matrix_copy.close(True) self.new_matrix.close(True) def test_export(self): self.test_load() self.new_matrix.export(csv_export_name) self.new_matrix.close(True) def test_nan_to_num(self): self.test_load() s = self.new_matrix.seed.sum() - self.new_matrix.seed[1, 1] m = self.new_matrix.mat.sum() - self.new_matrix.mat[1, 1] self.new_matrix.seed[1,1] = np.nan self.new_matrix.computational_view(['mat', 'seed']) self.new_matrix.nan_to_num() self.new_matrix.mat[1,1] = np.nan self.new_matrix.computational_view(['mat']) self.new_matrix.nan_to_num() if s != self.new_matrix.seed.sum(): self.fail('Total for seed matrix not maintained') if m != self.new_matrix.mat.sum(): self.fail('Total for mat matrix not maintained')
column_vector.create_empty(**args) column_vector.columns[:] = np.random.rand(zones)[:] * 1000 column_vector.index[:] = np.arange(zones)[:] # balance vectors column_vector.columns[:] = column_vector.columns[:] * (row_vector.rows.sum()/column_vector.columns.sum()) # seed matrix_procedures name_test = AequilibraeMatrix().random_name() args = {'file_name': name_test, 'zones': zones, 'matrix_names': ['seed']} matrix = AequilibraeMatrix() matrix.create_empty(**args) matrix.seed[:, :] = np.random.rand(zones, zones)[:,:] matrix.computational_view(['seed']) matrix.matrix_view[1,1] = np.nan matrix.index[:] = np.arange(zones)[:] class TestIpf(TestCase): def test_fit(self): # The IPF per se args = {'matrix': matrix, 'rows': row_vector, 'row_field': 'rows', 'columns': column_vector, 'column_field': 'columns', 'nan_as_zero': False} fratar = Ipf(**args)