Path: blob/main/tools/import/vissim/convert_vissimXML_flows_statRoutes.py
169679 views
#!/usr/bin/env python1# -*- coding: utf-8 -*-2# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo3# Copyright (C) 2015-2025 German Aerospace Center (DLR) and others.4# This program and the accompanying materials are made available under the5# terms of the Eclipse Public License 2.0 which is available at6# https://www.eclipse.org/legal/epl-2.0/7# This Source Code may also be made available under the following Secondary8# Licenses when the conditions for such availability set forth in the Eclipse9# Public License 2.0 are satisfied: GNU General Public License, version 210# or later which is available at11# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html12# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later1314# @file convert_vissimXML_flows_statRoutes.py15# @author Lukas Grohmann <[email protected]>16# @author Gerald Richter <[email protected]>17# @date Jun 09 20151819"""20Parses flows and static routes from a VISSIM .inpx file21and writes converted information to a SUMO routes (.rou.xml) file.22(see source documentation)2324example usage:25python3 convert_vissimXML_flows_statRoutes.py my_VISSIM_scenario.inpx my_VISSIM_scenario.net.xml26-o my_VISSIM_routes.rou.xml2728see also:29python3 convert_vissimXML_flows_statRoutes.py -h30"""31from __future__ import absolute_import32from __future__ import print_function3334import os35import sys36from xml.dom import minidom37from xml.dom.minidom import Document38import numpy as np39if 'SUMO_HOME' in os.environ:40sys.path.append(os.path.join(os.environ['SUMO_HOME'], 'tools'))41import sumolib # noqa424344def _dict_from_node_attributes(node):45"""takes xml node and returns a dict with its attributes46"""47return dict((attn, node.getAttribute(attn)) for attn in48node.attributes.keys())495051# FUNKTIONEN52def parse_flows(xmldoc):53"""parses the vehicleInput flows from the VISSIM data54:param xmldoc: input VISSIM xml55:type xmldoc: xml.dom.minidom.Document56:return: flow data by VISSIM start link id57:rtype: dict5859.. note:: time frames are converted from [ms] -> [s]60.. todo:: remove the redundant col2 in ['flow']61"""62flw_d = dict() # local flows dict63for v_input in xmldoc.getElementsByTagName('vehicleInput'):64v_input_d = _dict_from_node_attributes(v_input)65v_input_d["vehComp"] = []66v_input_d["volType"] = []67v_input_d["flow"] = []68for volume in v_input.getElementsByTagName('timeIntervalVehVolume'):69v_input_d["vehComp"].append(volume.getAttribute('vehComp'))70v_input_d["volType"].append(volume.getAttribute('volType'))71# keeping (timeInterval, volume, vehicle composition)72# time interval converted to [s]73v_input_d["flow"].append(74[float(volume.getAttribute('timeInt').split(" ")[1]) / 1000,75float(volume.getAttribute('volume')),76float(volume.getAttribute('vehComp'))]) # FIXME: nasty, redundant77v_input_d["flow"] = np.array(v_input_d["flow"])78# here goes a VISSIM linkId as key (& value)79flw_d[v_input_d["link"]] = v_input_d80return flw_d818283def parse_max_acc(xmldoc):84"""parses the vehicle acceleration distributions from the VISSIM data85:param xmldoc: input VISSIM xml86:type xmldoc: xml.dom.minidom.Document87:return: map of 1st acceleration function data point value by str(numeric id)88:rtype: dict89"""90acc_d = dict()91for max_acc in xmldoc.getElementsByTagName('maxAccelerationFunction'):92acc_d[max_acc.getAttribute('no')] = max_acc.getElementsByTagName(93'accelerationFunctionDataPoint')[0].getAttribute('y')94return acc_d959697def parse_speed_avg(xmldoc):98"""parses the vehicle speed distribution from the VISSIM data99:param xmldoc: input VISSIM xml100:type xmldoc: xml.dom.minidom.Document101:return: map of some speed averages by str(numeric id)102:rtype: dict103104.. note:: the average is only approximated105"""106spd_d = dict() # local speeds dict107for deSpeeDis in xmldoc.getElementsByTagName('desSpeedDistribution'):108# get mean speed109num = 0.110sum_val = 0.111data_points = deSpeeDis.getElementsByTagName('speedDistributionDataPoint')112for point in data_points:113num += 1114sum_val += float(point.getAttribute('x'))115spd_d[deSpeeDis.getAttribute('no')] = str((sum_val / num) / 3.6)116return spd_d117118119def parse_length(xmldoc):120"""parses the vehicle type lengths from the VISSIM data121:param xmldoc: input VISSIM xml122:type xmldoc: xml.dom.minidom.Document123:return: map of lengths by str(numeric type)124:rtype: dict125"""126len_d = dict()127model_d = dict()128# get model data129for model in xmldoc.getElementsByTagName('model2D3D'):130model_d[model.getAttribute('no')] = model.getElementsByTagName(131'model2D3DSegment')[0].getAttribute('length')132# calculate length data133for model_dist in xmldoc.getElementsByTagName('model2D3DDistribution'):134elements = model_dist.getElementsByTagName(135'model2D3DDistributionElement')136length = 0137total_probability = 0138for element in elements:139total_probability += float(element.getAttribute('share'))140for element in elements:141length += (142float(element.getAttribute('share')) / total_probability) * \143float(model_d[element.getAttribute('model2D3D')])144len_d[model_dist.getAttribute('no')] = str(length)145return len_d146147148def parse_veh_comp(xmldoc):149"""parses the vehicle composition from the VISSIM data150:param xmldoc: input VISSIM xml151:type xmldoc: xml.dom.minidom.Document152:return: relevant VISSIM vehicleComposition data153:rtype: dict of list of dict154"""155veh_cmp_d = dict() # local vehicle compositions' dict156for vehicle_comp in xmldoc.getElementsByTagName('vehicleComposition'):157rel_flows = vehicle_comp.getElementsByTagName(158'vehicleCompositionRelativeFlow')159flow_l = []160for flow in rel_flows:161flw_d = {162'desSpeedDistr': flow.getAttribute('desSpeedDistr'),163'rel_flow': flow.getAttribute('relFlow'),164'vehType': flow.getAttribute('vehType'),165}166flow_l.append(flw_d)167# list of dictionaries168veh_cmp_d[vehicle_comp.getAttribute('no')] = flow_l169return veh_cmp_d170171172def parse_vehicle_types(xmldoc, acc_d, length_d):173"""parses the vehicle types from the VISSIM data174:param xmldoc: input VISSIM xml175:type xmldoc: xml.dom.minidom.Document176:return: relevant VISSIM vehicle type data177:rtype: dict of dict178"""179veh_type_d = dict()180for veh_type in xmldoc.getElementsByTagName('vehicleType'):181type_d = {182'id': veh_type.getAttribute('no'),183'length': length_d[veh_type.getAttribute('model2D3DDistr')],184'acc': acc_d[veh_type.getAttribute('maxAccelFunc')],185}186veh_type_d[veh_type.getAttribute('no')] = type_d187return veh_type_d188189190# FIXME: not necessarily nicely done191def gen_verbinder_map(xmldoc):192"""produce dict with boolean values to check if a given link is a verbinder193:param xmldoc: input VISSIM xml194:type xmldoc: xml.dom.minidom.Document195:return: map of VISSIM link id -> bool flag if link is 'Verbinder'196:rtype: dict197"""198# simple implementation by static variable; xmldoc arg is in the way199# if not hasattr(gen_verbinder_map, "v_dic"):200# gen_verbinder_map.v_dic = dict() # doesn't exist yet, so initialize201is_verbinder_d = dict()202for link in xmldoc.getElementsByTagName("link"):203if len(link.getElementsByTagName("fromLinkEndPt")) > 0:204is_verbinder_d[link.getAttribute("no")] = True205else:206is_verbinder_d[link.getAttribute("no")] = False207# returning a dict...208return is_verbinder_d209210211def parse_routes(xmldoc, edge_id_list, verbinder_d):212"""parses the VISSIM route information of statically defined routes ONLY213:param xmldoc: input VISSIM xml214:type xmldoc: xml.dom.minidom.Document215:param edge_id_list: the name says it all; SUMO edge ids216:param verbinder_d: bool(verbinder status) of VISSIM link id217:type verbinder_d: dict218:return: routes by VISSIM start link id, with respective destination routes219:rtype: dict220221.. note:: time frames are converted from [ms] -> [s]222.. todo:: extend for non-static routes223"""224# create a list of just the split vissim edges (marked by ending char ']')225split_edge_list = [e for e in edge_id_list if e[-1] == ']']226rts_by_start_d = dict() # dictionary[start_link] = list(<Route>)227# loop over all routing decisions228for decision in xmldoc.getElementsByTagName('vehicleRoutingDecisionStatic'):229start_link = decision.getAttribute('link')230rts_by_start_d[start_link] = []231for vehRtStatic in decision.getElementsByTagName('vehicleRouteStatic'):232route_d = {233"start_link": start_link, # VISSIM id234"dest_link": vehRtStatic.getAttribute('destLink'), # VISSIM id235"r_id": vehRtStatic.getAttribute('no'),236"rel_flow": [],237"links": [start_link, ] # finally translated to SUMO ids238}239# split into separate time intervals' relative flow data240for tIrFlow in map(str.strip, str(vehRtStatic.getAttribute('relFlow')).split(',')):241if len(tIrFlow) == 0:242continue243temp = tIrFlow.split() # get "id", "tInterval:relFlow"244try:245tIrFlow = map(float, temp[1].split(':')) # grab [tInterval, relFlow]246except TypeError:247print('- WARNING - incomplete relative flow definition in inpx\n',248decision.toxml())249route_d["rel_flow"].append(list(tIrFlow))250tIrFlow = np.array(route_d["rel_flow"])251if len(tIrFlow) > 0:252tIrFlow[:, 0] /= 1000 # VISSIM time intervals [ms]->[s]253route_d["rel_flow"] = tIrFlow254else:255# create something.. 0 rows, 2 cols256# NOTE: better None, but takes some adaption work257route_d["rel_flow"] = np.empty((0, 2), dtype="f")258259# get all the intermediary links in their sumo representation260for link in vehRtStatic.getElementsByTagName('intObjectRef'):261link_key = link.getAttribute('key')262if verbinder_d[link_key]:263# exclude VISSIM connectors (usually id > 10k)264continue265# collect them all in VISSIM scheme first, then replace them266route_d["links"].append(link_key)267route_d["links"].append(route_d["dest_link"])268269# translate to sumo edge ids270sumo_links = []271for link_key in route_d["links"]:272if link_key in edge_id_list:273# key is found unmodified in edge_id_list274sumo_links.append(link_key)275else:276# extension list *IS* ordered by its splitting sequence as generated277sumo_links.extend(e for e in split_edge_list278if e.startswith(link_key + '['))279# update with sumo ids info280route_d["links"] = sumo_links281282# add route object to dictionary283rts_by_start_d[start_link].append(route_d)284return rts_by_start_d285286287def calc_route_probability(routes_by_start_d, flow_d):288"""computes the route probabilities289:param routes_by_start_d: map by start link id with route dicts as values290:type routes_by_start_d: dict291:param flow_d: vissim vehicle in-flow data292:type flow_d: dict293"""294for start_link, sl_routes in routes_by_start_d.items():295if start_link not in flow_d:296# we got no in-flow data for that route's start link297print('- skipping probability calc tfor route without flow def. for VISSIM start link id:', start_link)298continue299# set 0 vectors for all time frames300absolute_flow = flow_d[start_link]["flow"][:, 1]301veh_comp = flow_d[start_link]["vehComp"]302# time frames must have the same limits as flows, as checked before303# therefor all route flows for 1 start link must also have same limits304# get all the startlink-route rel.flows-by-time-window lined up305sl_rt_relF = np.stack([rt['rel_flow'] for rt in sl_routes])306# all summed rel.flows by timeframe307# sl_sum_relF = sl_rt_relF.sum(axis=0)[:, 1:] # keep shape (n x timeframes)308sl_sum_relF = sl_rt_relF.sum(axis=0)[:, 1] # shape (timeframes, )309for route in routes_by_start_d[start_link]:310# set the vehicle type for each route311route["type"] = veh_comp312route["probability"] = np.zeros_like(absolute_flow)313# get a selector for all summed up flows > 0 (= relevant)314comp_flow_sel = sl_sum_relF > 0.315route["probability"][comp_flow_sel] = \316(route["rel_flow"][comp_flow_sel, 1] / sl_sum_relF[comp_flow_sel])317318319def validate_rel_flow(routes_by_start_d, flow_d):320"""checks if a relative flow is missing and completes it if necessary321essentially fixing a VISSIM inp -> inpx conversion bug322:param routes_by_start_d: map by start link id with route dicts as values323:type routes_by_start_d: dict324:param flow_d: vissim vehicle in-flow data325:type flow_d: dict326327.. note:: *modifies* routes_by_start_d328"""329# VISSIM BUG!!: Relative Zuflüsse mit dem Wert 1.0 gehen bei der330# Konversion von .inp zu .inpx verloren331332# compare all rel_flows with the reference flow333# get all time frame limits from all routes334# np.concatenate([rt['rel_flow'] for rtl in routes_by_start_d.values() for rt in rtl])335for start_link, sl_routes in routes_by_start_d.items():336if start_link not in flow_d:337# should we remove the routes entry ?338print('- skipping flow validation for route without flow def. for VISSIM start link id:', start_link)339# CHECK: is this ok with later steps ?340continue341# grab all the time window starts from the flows342# NOTE: need slice here due to redundant veh_comp col.343ref_time_shape = flow_d.get(start_link)["flow"][:, :2]344ref_time_shape[:, 1] = 1. # set to default (VISSIM inp-> inpx BUG)345for route in sl_routes:346# check if there is a relative flow def. at all347if len(route["rel_flow"]) == 0:348# if not, append default349route["rel_flow"] = ref_time_shape.copy()350continue351else:352if not np.array_equal(ref_time_shape[:, 0], route["rel_flow"][:, 0]):353orig = dict(route["rel_flow"])354flow = ref_time_shape.copy()355for i, (time, _) in enumerate(flow):356flow[i][1] = orig.get(time, 0)357route["rel_flow"] = flow358# copy back modifications359routes_by_start_d[start_link] = sl_routes360361362def create_vTypeDistribution_elems(veh_comp_d, veh_type_d, speed_d, root):363"""append the vehicle distribution data to the given dom document364:param veh_comp_d:365:type veh_comp_d: dict366:param veh_type_d:367:type veh_type_d: dict368:param speed_d:369:type speed_d: dict370:param root: XML root element to append children to371372.. note:: *modifies/extends* XML root element373"""374# iterate vehicle compositions375for c_id, comps in veh_comp_d.items():376v_type_dist = root.ownerDocument.createElement("vTypeDistribution")377v_type_dist.setAttribute("id", c_id)378root.appendChild(v_type_dist)379for comp in comps:380v_type = root.ownerDocument.createElement("vType")381v_type.setAttribute(382"id",383"t{}_D{}".format(384veh_type_d[comp["vehType"]]["id"],385c_id))386v_type.setAttribute("accel", veh_type_d[comp["vehType"]]["acc"])387v_type.setAttribute("length",388veh_type_d[comp["vehType"]]["length"])389v_type.setAttribute("probability", comp["rel_flow"])390v_type.setAttribute("maxSpeed", speed_d[comp["desSpeedDistr"]])391v_type_dist.appendChild(v_type)392# return route_doc393394395def create_routeDistribution_elems(routes_by_start_d, root):396"""append the route distribution data into the given dom document397:param routes_by_start_d: map by start link id with route dicts as values398:type routes_by_start_d: dict399:param root: XML root element to append children to400401.. note:: *modifies/extends* XML root element402"""403# iterating by VISSIM link id404validDists = set()405for start_link in routes_by_start_d:406if start_link not in flow_d:407# no flow, no go408print('- skipping route dist. gen for route without flow def. for VISSIM start link id:', start_link)409continue410if len(routes_by_start_d[start_link]) == 0:411continue412ref_time = flow_d[start_link]["flow"][:, 0]413for ic, time in enumerate(ref_time):414route_dist = root.ownerDocument.createElement("routeDistribution")415# just a name416distID = "_".join([start_link, str(time)])417route_dist.setAttribute("id", distID)418for route in routes_by_start_d[start_link]:419if np.abs(route["probability"][ic]) != 0:420route_node = root.ownerDocument.createElement("route")421route_node.setAttribute("id", route["r_id"])422route_node.setAttribute("edges",423" ".join(route["links"]))424route_node.setAttribute("probability",425str(np.abs(426route["probability"][ic])))427route_dist.appendChild(route_node)428if route_dist.hasChildNodes():429root.appendChild(route_dist)430validDists.add(distID)431return validDists432433434def create_flow_elems(routes_by_start_d, flow_d, validDists, root):435"""append the flow data to the given dom document436:param routes_by_start_d: map by start link id with route dicts as values437:type routes_by_start_d: dict438:param flow_d: vissim vehicle in-flow data439:type flow_d: dict440:param root: XML root element to append children to441442.. note:: *modifies/extends* XML root element443"""444sim_end = inpx_doc.getElementsByTagName("simulation")[0].getAttribute("simPeriod")445dom_flow_l = []446for start_link in routes_by_start_d:447if start_link not in flow_d:448# we got no in-flow data for that route's start link449print('- skipping flow gen for route without flow def. for VISSIM start link id:', start_link)450continue451if len(routes_by_start_d[start_link]) == 0:452print('- found no routes by start link:', start_link)453continue454flows = flow_d[start_link]["flow"]455# iterate over all the time frame starts from the flows456ref_time = flows[:, 0]457for index, time in enumerate(ref_time):458distID = "_".join([start_link, str(time)])459in_flow = [fl for fl in flow_d[start_link]["flow"] if460fl[0] == time][0]461if in_flow[1] > 0 and distID in validDists:462flow = root.ownerDocument.createElement("flow")463flow.setAttribute("id", "fl{}_st{}".format(start_link,464time))465flow.setAttribute("color", "1,1,0")466flow.setAttribute("begin", str(time))467if index < len(ref_time) - 1 and len(ref_time) > 1:468flow.setAttribute("end",469str(time + ref_time[index + 1]))470else:471flow.setAttribute("end", sim_end)472flow.setAttribute("vehsPerHour", str(in_flow[1]))473flow.setAttribute("type", str(int(in_flow[2])))474flow.setAttribute('route', distID)475dom_flow_l.append(flow)476dom_flow_l = sorted(dom_flow_l,477key=lambda dom: float(dom.getAttribute("begin")))478for dom_obj in dom_flow_l:479root.appendChild(dom_obj)480# return route_doc481482483# MAIN484if __name__ == '__main__':485op = sumolib.options.ArgumentParser(486description='road network conversion utility for static route flows'487' (VISSIM.inpx to SUMO); generates SUMO routes definition file from'488' given inpx and derived (by netconvert) SUMO net.')489op.add_argument('--output-file', '-o', default='routes.rou.xml', category="output", type=op.route_file,490help='output file name (default: %(default)s)')491op.add_argument('--vissim-file', '-V', dest="vissim_file", category="input", required=True, type=op.file,492help='VISSIM inpx file path')493op.add_argument('--sumo-net-file', '-n', dest="sumo_net_file", category="input", required=True, type=op.net_file,494help='SUMO net file path')495args = op.parse_args()496# print("\n", args, "\n")497498#499# Input data ##########500#501print('\n---\n\n* loading VISSIM net:\n\t', args.vissim_file)502inpx_doc = minidom.parse(args.vissim_file)503print('\n---\n\n* loading SUMO net:\n\t', args.sumo_net_file,)504sumo_doc = minidom.parse(args.sumo_net_file)505506print('+ building edge list...')507# for all normal edges508sumo_edge_ids = [edge.getAttribute("id") for edge in509sumo_doc.getElementsByTagName('edge')510if not edge.hasAttribute("function")]511print('\tOK.')512513print('+ building "Verbinder"("connector") info...')514# to check if a link is a verbinder515verbinder_flag = gen_verbinder_map(inpx_doc)516print('\tOK.')517518print('\n---')519#520# Vehicle Speeds, distributions, types ##########521#522print('* parsing speeds...')523# parse vehicle type data524speed_d = parse_speed_avg(inpx_doc)525print('* parsing vehicle distributions...')526# get the vehicle distribution527vehicle_comp_d = parse_veh_comp(inpx_doc)528print('* parsing vehicle types...')529# parse vehTypes and combine the information with acceleration and length data530vehicle_type_d = parse_vehicle_types(inpx_doc, parse_max_acc(inpx_doc),531parse_length(inpx_doc))532print('OK.\n---')533534#535# Flows and Routes ##########536#537# TODO: maybe make flows and routes conversion switchable by option ?538print('* parsing vehicle in-flow definitions...')539# parse flows540flow_d = parse_flows(inpx_doc)541print('* parsing vehicle routes...')542# parse routes543routes_by_start_d = parse_routes(inpx_doc, sumo_edge_ids, verbinder_flag)544print('+ validating relative flows...')545# validate relative flows546validate_rel_flow(routes_by_start_d, flow_d)547print('+ setting route branching probabilities...')548# computes the probability for each route549calc_route_probability(routes_by_start_d, flow_d)550print('OK.\n---')551552#553# XML generation ##########554#555print('* output routes generation...')556# create dom document and define routes + flows557result_doc = Document()558routes_Elem = result_doc.createElement("routes")559result_doc.appendChild(routes_Elem)560561create_vTypeDistribution_elems(vehicle_comp_d, vehicle_type_d, speed_d, routes_Elem)562print('-' * 3)563validDists = create_routeDistribution_elems(routes_by_start_d, routes_Elem)564print('-' * 3)565create_flow_elems(routes_by_start_d, flow_d, validDists, routes_Elem)566print('OK.\n---')567568print('* writing output:')569# write the data into a .rou.xml file570out_Fn = args.output_file571if not out_Fn.endswith('.xml'):572out_Fn += '.xml'573with open(out_Fn, "w") as ofh:574result_doc.writexml(ofh, addindent=' ', newl='\n')575ofh.close()576print('. data written to:\n\t', out_Fn)577578579