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}
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 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
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_()
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]