Skip to content

Commit

Permalink
Revision of AequilibraE#250 (Adding QGIS processing provider)
Browse files Browse the repository at this point in the history
Should be ready for merge, full revision of "old" PR AequilibraE#250 :
* Adding processing provider (translatable)
* Include traffic assignment from yaml
  • Loading branch information
Art-Ev committed May 23, 2024
1 parent 439f223 commit 59583a3
Show file tree
Hide file tree
Showing 11 changed files with 752 additions and 3 deletions.
5 changes: 3 additions & 2 deletions qaequilibrae/metadata.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
65 changes: 65 additions & 0 deletions qaequilibrae/modules/processing_provider/Add_connectors.py
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 qaequilibrae/modules/processing_provider/AssignFromYaml.py
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 qaequilibrae/modules/processing_provider/MatrixFromLayer.py
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)
Loading

0 comments on commit 59583a3

Please sign in to comment.