forked from AequilibraE/qaequilibrae
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Revision of AequilibraE#250 (Adding QGIS processing provider)
Should be ready for merge, full revision of "old" PR AequilibraE#250 : * Adding processing provider (translatable) * Include traffic assignment from yaml
- Loading branch information
Showing
11 changed files
with
752 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,13 +4,14 @@ qgisMinimumVersion=3.34 | |
qgisMaximumVersion=3.99 | ||
description=Transportation modeling toolbox for QGIS | ||
about=QAequilibraE is the GUI for AequilibraE, a transportation modeling package designed to be an open-source alternative to traditional commercial packages. It is a comprehensive set of tools for modeling and visualization, including incredibly fast equilibrium traffic assignment, synthetic gravity models, network editing, and GTFS importer. http://www.aequilibrae.com/. | ||
version=1.0.1 | ||
version=1.0.2 | ||
author=Pedro Camargo | ||
[email protected] | ||
repository= https://github.com/AequilibraE/QAequilibraE | ||
tracker=https://github.com/aequilibrae/QAequilibraE/issues | ||
icon=icon.png | ||
experimental=False | ||
hasProcessingProvider=yes | ||
experimental=True | ||
homepage=https://www.aequilibrae.com/qgis | ||
|
||
|
||
|
65 changes: 65 additions & 0 deletions
65
qaequilibrae/modules/processing_provider/Add_connectors.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import importlib.util as iutil | ||
import pandas as pd | ||
import sys | ||
from qgis.core import QgsProcessingAlgorithm, QgsProcessingMultiStepFeedback | ||
from qgis.core import QgsProcessingParameterFile, QgsProcessingParameterNumber, QgsProcessingParameterString | ||
|
||
from .translatableAlgo import TranslatableAlgorithm | ||
|
||
|
||
class AddConnectors(TranslatableAlgorithm): | ||
|
||
def initAlgorithm(self, config=None): | ||
self.addParameter(QgsProcessingParameterNumber('nb_conn', self.tr('Desired number of connectors'), type=QgsProcessingParameterNumber.Integer, minValue=1, maxValue=10, defaultValue=1)) | ||
self.addParameter(QgsProcessingParameterString('mode', self.tr('Mode to connect (only one at a time)'), multiLine=False, defaultValue='c')) | ||
self.addParameter(QgsProcessingParameterFile('PrjtPath', self.tr('AequilibraE project'), behavior=QgsProcessingParameterFile.Folder, defaultValue='D:/')) | ||
|
||
def processAlgorithm(self, parameters, context, model_feedback): | ||
feedback = QgsProcessingMultiStepFeedback(2, model_feedback) | ||
|
||
# Checks if we have access to aequilibrae library | ||
if iutil.find_spec("aequilibrae") is None: | ||
sys.exit(self.tr('AequilibraE library not found')) | ||
|
||
from aequilibrae import Project | ||
feedback.pushInfo(self.tr('Connecting to aequilibrae project')) | ||
project = Project() | ||
project.open(parameters['PrjtPath']) | ||
|
||
all_nodes= project.network.nodes | ||
nodes_table= all_nodes.data | ||
|
||
feedback.pushInfo(' ') | ||
feedback.setCurrentStep(1) | ||
|
||
# Adding connectors | ||
nb_conn=parameters['nb_conn'] | ||
mode=parameters['mode'] | ||
feedback.pushInfo(self.tr(f'Adding {nb_conn} connectors when none exists for mode "{mode}"')) | ||
for idx, node in nodes_table.query("is_centroid == 1").iterrows(): | ||
curr=all_nodes.get(node.node_id) | ||
curr.connect_mode(curr.geometry.buffer(0.01), mode_id=mode, connectors=nb_conn) | ||
feedback.pushInfo(' ') | ||
feedback.setCurrentStep(2) | ||
|
||
project.close() | ||
output_file=parameters['PrjtPath'] | ||
return {'Output': output_file} | ||
|
||
def name(self): | ||
return self.tr('Add connectors') | ||
|
||
def displayName(self): | ||
return self.tr('Add connectors') | ||
|
||
def group(self): | ||
return self.tr('1_Network') | ||
|
||
def groupId(self): | ||
return self.tr('1_Network') | ||
|
||
def shortHelpString(self): | ||
return self.tr("Go through all the centroids and add connectors only if none exists for the chosen mode") | ||
|
||
def createInstance(self): | ||
return AddConnectors(self.tr) |
163 changes: 163 additions & 0 deletions
163
qaequilibrae/modules/processing_provider/AssignFromYaml.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import importlib.util as iutil | ||
import sys | ||
from qgis.core import QgsProcessingMultiStepFeedback | ||
from qgis.core import QgsProcessingParameterFile | ||
|
||
from .translatableAlgo import TranslatableAlgorithm | ||
|
||
|
||
class TrafficAssignYAML(TranslatableAlgorithm): | ||
|
||
def initAlgorithm(self, config=None): | ||
self.addParameter(QgsProcessingParameterFile('confFile', self.tr('Configuration file (.yaml)'), behavior=QgsProcessingParameterFile.File, fileFilter=self.tr('Assignment configuration file (*.yaml)'), defaultValue=None)) | ||
|
||
def processAlgorithm(self, parameters, context, model_feedback): | ||
feedback = QgsProcessingMultiStepFeedback(5, model_feedback) | ||
|
||
# Checks if we have access to aequilibrae library | ||
if iutil.find_spec("aequilibrae") is None: | ||
sys.exit(self.tr('AequilibraE library not found')) | ||
|
||
from aequilibrae.paths import TrafficAssignment, TrafficClass | ||
from aequilibrae.project import Project | ||
from aequilibrae.matrix import AequilibraeMatrix | ||
import yaml | ||
|
||
feedback.pushInfo(self.tr('Getting parameters from input yaml file...')) | ||
pathfile=parameters['confFile'] | ||
with open(pathfile,'r') as f: | ||
params=yaml.safe_load(f) | ||
feedback.pushInfo(' ') | ||
feedback.setCurrentStep(1) | ||
|
||
feedback.pushInfo(self.tr('Opening project and setting up traffic classes...')) | ||
# Opening project | ||
project = Project() | ||
project.open(params["Project"]) | ||
|
||
# Creating graph | ||
project.network.build_graphs() | ||
|
||
# Creating traffic classes | ||
traffic_classes=[] | ||
feedback.pushInfo(str(len(params["Traffic_classes"]))+self.tr(' traffic classes have been found in config file :')) | ||
for classes in params["Traffic_classes"]: | ||
for traffic in classes: | ||
|
||
#Getting matrix | ||
demand = AequilibraeMatrix() | ||
demand.load(classes[traffic]["matrix_path"]) | ||
demand.computational_view([classes[traffic]["matrix_core"]]) | ||
|
||
# Getting graph | ||
graph = project.network.graphs[classes[traffic]["network_mode"]] | ||
graph.set_graph("travel_time") | ||
graph.set_blocked_centroid_flows(False) | ||
|
||
if "skims" in classes[traffic] and classes[traffic]["skims"] is not None: | ||
skims=[sk.strip() for sk in classes[traffic]["skims"].split(",")] | ||
graph.set_skimming(skims) | ||
|
||
# Setting class | ||
assigclass=TrafficClass(name=traffic, graph=graph, matrix=demand) | ||
assigclass.set_pce(classes[traffic]["pce"]) | ||
|
||
if "fixed_cost" in classes[traffic] and classes[traffic]["fixed_cost"] is not None : | ||
if "vot" in classes[traffic] and (type(classes[traffic]["vot"])==int or type(classes[traffic]["vot"])==float): | ||
assigclass.set_fixed_cost(classes[traffic]["fixed_cost"]) | ||
assigclass.set_vot(classes[traffic]["vot"]) | ||
else: | ||
sys.exit("error: fixed_cost must come with a correct value of time") | ||
|
||
# Adding class | ||
feedback.pushInfo(' - '+traffic+' '+str(classes[traffic])) | ||
traffic_classes.append(assigclass) | ||
feedback.pushInfo(' ') | ||
feedback.setCurrentStep(2) | ||
|
||
# Setting up assignment | ||
feedback.pushInfo(self.tr('Setting up assignment...')) | ||
feedback.pushInfo(str(params["Assignment"])) | ||
assig = TrafficAssignment() | ||
assig.set_classes(traffic_classes) | ||
assig.set_vdf(params["Assignment"]["vdf"]) | ||
assig.set_vdf_parameters({"alpha": params["Assignment"]["alpha"], "beta": params["Assignment"]["beta"]}) | ||
assig.set_capacity_field(params["Assignment"]["capacity_field"]) | ||
assig.set_time_field(params["Assignment"]["time_field"]) | ||
|
||
assig.set_algorithm(params["Assignment"]["algorithm"]) | ||
assig.max_iter = params["Assignment"]["max_iter"] | ||
assig.rgap_target = params["Assignment"]["rgap"] | ||
|
||
feedback.pushInfo(' ') | ||
feedback.setCurrentStep(3) | ||
|
||
# Running assignment | ||
feedback.pushInfo(self.tr('Running traffic assignment...')) | ||
assig.execute() | ||
feedback.pushInfo(' ') | ||
feedback.setCurrentStep(4) | ||
|
||
# Saving outputs | ||
feedback.pushInfo(self.tr('Assignment completed, saving outputs...')) | ||
feedback.pushInfo(str(assig.report())) | ||
assig.save_results(params["Run_name"]) | ||
assig.save_skims(params["Run_name"] , which_ones="all", format="omx") | ||
feedback.pushInfo(' ') | ||
feedback.setCurrentStep(5) | ||
|
||
project.close() | ||
|
||
return {'Output': 'Traffic assignment successfully completed'} | ||
|
||
def name(self): | ||
return self.tr('Traffic assignment from a config file') | ||
|
||
def displayName(self): | ||
return self.tr('Traffic assignment from a config file') | ||
|
||
def group(self): | ||
return self.tr('3_Assignment') | ||
|
||
def groupId(self): | ||
return self.tr('3_Assignment') | ||
|
||
def shortHelpString(self): | ||
return self.tr(""" | ||
Run a traffic assignment using a yaml configuration file. Example of valide configuration file: | ||
"" | ||
Project: D:/AequilibraE/Project/ | ||
Run_name: sce_from_yaml | ||
Traffic_classes: | ||
- car: | ||
matrix_path: D:/AequilibraE/Project/matrices/demand.aem | ||
matrix_core: car | ||
network_mode: c | ||
pce: 1 | ||
blocked_centroid_flows: True | ||
skims: travel_time, distance | ||
- truck: | ||
matrix_path: D:/AequilibraE/Project/matrices/demand.aem | ||
matrix_core: truck | ||
network_mode: c | ||
pce: 2 | ||
fixed_cost: toll | ||
vot: 12 | ||
blocked_centroid_flows: True | ||
Assignment: | ||
algorithm: bfw | ||
vdf: BPR2 | ||
alpha: 0.15 | ||
beta: power | ||
capacity_field: capacity | ||
time_field: travel_time | ||
max_iter: 250 | ||
rgap: 0.00001 | ||
"" | ||
""") | ||
|
||
def createInstance(self): | ||
return TrafficAssignYAML(self.tr) |
128 changes: 128 additions & 0 deletions
128
qaequilibrae/modules/processing_provider/MatrixFromLayer.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
from qgis.core import QgsProcessingAlgorithm, QgsProcessingMultiStepFeedback | ||
from qgis.core import QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject | ||
from qgis.core import QgsProcessingParameterVectorLayer, QgsProcessingParameterField, QgsProcessingParameterMapLayer, QgsProcessingParameterFile, QgsProcessingParameterString, QgsProcessingParameterDefinition | ||
from .translatableAlgo import TranslatableAlgorithm | ||
|
||
import importlib.util as iutil | ||
import os, sys | ||
import numpy as np | ||
import pandas as pd | ||
from scipy.sparse import coo_matrix | ||
|
||
class MatrixFromLayer(TranslatableAlgorithm): | ||
|
||
def initAlgorithm(self, config=None): | ||
self.addParameter(QgsProcessingParameterMapLayer('matrix_layer', self.tr('Layer containing a matrix in list format'))) | ||
self.addParameter(QgsProcessingParameterField('origin', self.tr('Origin field'), type=QgsProcessingParameterField.Numeric, parentLayerParameterName='matrix_layer', allowMultiple=False, defaultValue='origin')) | ||
self.addParameter(QgsProcessingParameterField('destination', self.tr('Destination field'), type=QgsProcessingParameterField.Numeric, parentLayerParameterName='matrix_layer', allowMultiple=False, defaultValue='destination')) | ||
self.addParameter(QgsProcessingParameterField('value', self.tr('Value field'), type=QgsProcessingParameterField.Numeric, parentLayerParameterName='matrix_layer', allowMultiple=False, defaultValue='value')) | ||
self.addParameter(QgsProcessingParameterString('FileName', self.tr('Name your output file'), multiLine=False, defaultValue='')) | ||
self.addParameter(QgsProcessingParameterFile('OutputFold', self.tr('Output folder'), behavior=QgsProcessingParameterFile.Folder, fileFilter='Tous les fichiers (*.*)', defaultValue='D:/')) | ||
advparams = [QgsProcessingParameterString('MatrixName', self.tr('Name of your matrix'), optional=True, multiLine=False, defaultValue=''), | ||
QgsProcessingParameterString('MatrixDescription', self.tr('Something usefull to describe your matrix'), optional=True, multiLine=False, defaultValue=''), | ||
QgsProcessingParameterString('CoreName', self.tr('Name for the core of your matrix'), multiLine=False, defaultValue='Value') | ||
] | ||
for param in advparams: | ||
param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced) | ||
self.addParameter(param) | ||
|
||
def processAlgorithm(self, parameters, context, model_feedback): | ||
feedback = QgsProcessingMultiStepFeedback(3, model_feedback) | ||
|
||
# Checks if we have access to aequilibrae library | ||
if iutil.find_spec("aequilibrae") is None: | ||
sys.exit(self.tr('AequilibraE library not found')) | ||
|
||
from aequilibrae.matrix import AequilibraeMatrix | ||
|
||
origin=parameters['origin'] | ||
destination=parameters['destination'] | ||
value=parameters['value'] | ||
|
||
matrix_name=parameters['MatrixName'] | ||
matrix_description=parameters['MatrixDescription'] | ||
core_name=[parameters['CoreName']] | ||
|
||
output_folder=parameters['OutputFold'] | ||
output_name=parameters['FileName'] | ||
filename=os.path.join(output_folder,output_name+'.aem') | ||
|
||
# Import layer as a pandas df | ||
feedback.pushInfo(self.tr('Importing the layer from QGIS :')) | ||
layer = self.parameterAsVectorLayer(parameters, 'matrix_layer', context) | ||
cols=[origin,destination,value] | ||
datagen = ([f[col] for col in cols] for f in layer.getFeatures()) | ||
matrix = pd.DataFrame.from_records(data=datagen, columns=cols) | ||
feedback.pushInfo(str(matrix.head(5))) | ||
feedback.pushInfo('...') | ||
feedback.pushInfo('') | ||
feedback.setCurrentStep(1) | ||
|
||
# Getting all zones | ||
all_zones = np.array(sorted(list(set( list(matrix[origin].unique()) + list(matrix[destination].unique()))))) | ||
num_zones = all_zones.shape[0] | ||
idx = np.arange(num_zones) | ||
|
||
# Creates the indexing dataframes | ||
origs = pd.DataFrame({"from_index": all_zones, "from":idx}) | ||
dests = pd.DataFrame({"to_index": all_zones, "to":idx}) | ||
|
||
# adds the new index columns to the pandas dataframe | ||
matrix = matrix.merge(origs, left_on=origin, right_on='from_index', how='left') | ||
matrix = matrix.merge(dests, left_on=destination, right_on='to_index', how='left') | ||
|
||
agg_matrix = matrix.groupby(['from', 'to']).sum() | ||
|
||
# returns the indices | ||
agg_matrix.reset_index(inplace=True) | ||
|
||
# Creating the aequilibrae matrix file | ||
mat = AequilibraeMatrix() | ||
mat.name=matrix_name | ||
mat.description=matrix_description | ||
|
||
mat.create_empty(file_name = filename, | ||
zones = num_zones, | ||
matrix_names = core_name, | ||
memory_only = False) | ||
mat.index[:] = all_zones[:] | ||
|
||
m = coo_matrix((agg_matrix[value], (agg_matrix['from'], agg_matrix['to'])), shape=(num_zones, num_zones)).toarray().astype(np.float64) | ||
mat.matrix[core_name[0]][:,:] = m[:,:] | ||
feedback.pushInfo(self.tr(f'Matrix imported as a {num_zones}x{num_zones} matrix')) | ||
feedback.pushInfo(' ') | ||
feedback.setCurrentStep(2) | ||
|
||
feedback.pushInfo(self.tr('A final sweep after the work...')) | ||
output=mat.name+", "+mat.description+" ("+filename+")" | ||
mat.save() | ||
mat.close() | ||
del matrix | ||
|
||
feedback.pushInfo(' ') | ||
feedback.setCurrentStep(3) | ||
|
||
return {'Output': output} | ||
|
||
def name(self): | ||
return self.tr('Create a .aem matrix file from a layer') | ||
|
||
def displayName(self): | ||
return self.tr('Create a .aem matrix file from a layer') | ||
|
||
def group(self): | ||
return self.tr('2_Matrix') | ||
|
||
def groupId(self): | ||
return self.tr('2_Matrix') | ||
|
||
def shortHelpString(self): | ||
return self.tr(""" | ||
Save a layer as a .aem file : | ||
- the original matrix stored in the layer needs to be in list format | ||
- Origin and destination fields need to be integers | ||
- Value field can be integer or real | ||
""") | ||
|
||
def createInstance(self): | ||
return MatrixFromLayer(self.tr) |
Oops, something went wrong.