Sumo入门和Traci接口使用

正好项目中用到Sumo软件, 发现网上关于sumo的中文材料非常少, 所以我想记录一些自己使用sumo过程中的经验和教训;

SUMO的官方网站是 https://sumo.dlr.de/pydoc/
本文大部分代码相关内容都来源于此网站

一.Sumo的安装

我的环境是macos
安装可以直接使用homebrew安装
brew install sumo
具体的安装教程在sumo官网有详细说明;
mac版的安装在这个网址
安装完后记得要在bash-profile(bash)或者zshrc中(zsh)设置SUMO_HOME, 我这里的配置是这样:

#设置sumo, 这个是用homebrew装的
export SUMO_HOME="/usr/local/opt/sumo/share/sumo"

当一切都配置完成之后, 应该可以在终端使用命令sumo看到以下内容

~->sumo
Eclipse SUMO Version 1.3.1
 Build features: Darwin-17.7.0 x86_64 Clang 10.0.0.10001044 Release Proj GUI
 Copyright (C) 2001-2019 German Aerospace Center (DLR) and others; https://sumo.dlr.de
 License EPL-2.0: Eclipse Public License Version 2 <https://eclipse.org/legal/epl-v20.html>
 Use --help to get the list of options.

也可以使用sumo-gui命令进入sumo的gui客户端, 在mac中是基于XQuartz的

二.Sumo地图导入

我当时参考了这篇博文的内容veins车载通信仿真框架(2)–SUMO地图替换
大家可以去看一下这篇博文, 我简单介绍一下流程:
1.在OpenStreetMap网站上导出你想要研究的区域的地图, 在网页左上方有导出按钮, 然后选择区域之后就可以下载地图文件了, 应该是一个osm文件, 比如map.osm
2.在获得osm文件之后, 我们要用我们研究的地图替换sumo地图中的默认地图

  • 第一步, 根据osm文件生成.net.xml道路文件, 进入osm文件所在的目录下, 使用命令
netconvert --osm-files map.osm -o map.net.xml

此时我们会得到一个map.net.xml文件, 在这一步, 我们就可以在终端中输入sumo-gui命令打开gui软件, 然后File - open network 然后选中我们生成的net.xml地图, 就可以在软件中看到我们刚才下载的地图了

  • 第二步, 在刚才得到的地图中加入车辆, 因为我是要研究车流量相关的内容, 所以得到一个带车流的地图才有意义, 我们要生成一个.rou.xml车辆行为文件, 首先利用脚本randomTrips.py生成一个.trip.xml文件, 在这个文件中记录了随机生成的车辆的"旅程", 也就是每一辆车从哪儿开到哪儿, 至于具体的路线还是使用randomTrips脚本生成起点到终点的最短路径, 所以这一步使用2个命令
# 这一步是生成trips文件
/usr/local/opt/sumo/share/sumo/tools/randomTrips.py -n map.net.xml -e 100 -l
#生成车的路径行为xml的脚本, 
#每p个步长生成1个, 
#binomial二项分布系数是8, 1的时候退化成伯努利分布, 
#s是种子数, 
#e结束时间, 
#l是根据道路长度来划分权重, L是根据车道数划分权重, 我这里选的是l;
/usr/local/Cellar/sumo/1.3.1/share/sumo/tools/randomTrips.py -n map.net.xml -r map.rou.xml -e 3600 -l --binomial=8 -p 5 -s 830 -v
  • 第三步, 生成.poly.xml地形文件
polyconvert --net-file map.net.xml --osm-files map.osm -o map.poly.xml
  • 生成.sumo.cfg文件
<?xml version="1.0" encoding="iso-8859-1"?>
 
<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://sumo.sf.net/xsd/sumoConfiguration.xsd">
 
    <input>
        <net-file value="map.net.xml"/>
        <route-files value="map.rou.xml"/>
        <additional-files value="map.poly.xml"/>
    </input>
 
    <time>
        <begin value="0"/>
        <end value="1000"/>
        <step-length value="0.1"/>
    </time>
 
    <report>
        <no-step-log value="true"/>
    </report>
 
    <gui_only>
        <start value="true"/>
    </gui_only>
 
</configuration>

这个文件要和net, poly, rou那些文件放一起;

简单总结一下, net文件存了路网的节点和道路信息, rou存储了道路中所有车辆的信息, 比如车速, 多少辆车, 每辆车从哪儿开到哪儿这些信息, poly存储的是地形信息;

三.Traci接口

这个Traci接口是用来和Sumo模拟器通信的, 因为你不可能总是在sumo-gui里点图形化界面, 肯定得通过python, java之类的语言来和sumo通信, 靠的就是traci接口;

我尝试了java和python两种语言, java给了一个webservice的接口TraaS和一个tra4j的接口, 但是这两个接口都不太好用, 文档也不全, 真的要做开发的话建议可以尝试python的接口;

1.java接口

先讲一下用java来连sumo的过程中我遇到的一些问题;

首先呢说明一下那个tra4j很简单, 但是也是在不好用, 这里只介绍traas;

这里是traas的源码, 在github上
https://github.com/eclipse/sumo/tree/master/tools/contributed/traas

由于我没找到任何文档…只能通过猜测来用这个库了, 可能是通过运行traas/src/main/java/de/tudresden/ws/WebService.java这个类里面的main函数, 建一个webservice项目, 然后重新建一个jws客户端去访问这个服务, 和sumo通信;
然后其他的一些功能比如获取车辆位置什么的, 都可以通过源码里给的一些接口来获取, 但是我在这里当时遇到了一些问题, 甚至连dostep都无法进行;

2.python接口

这里是python接口的pydoc https://sumo.dlr.de/pydoc/
这里面都有比较详细对方法的说明;
有不太好理解的方法都可以去官网查到, 我这里简单介绍一下:

首先, 我这里先导入traci库, 还有一些其他以后可能会用到的库

# coding=utf-8
import sys
import random
import sumolib
import traci  # noqa
import csv

然后最基本的操作, 就是让程序跑起来

# coding=utf-8
import traci  # noqa

traci.start(["sumo-gui", "-c", "/location/map.sumo.cfg", "--emission-output", "emission"], port=7911)
while traci.simulation.getMinExpectedNumber() > 0:
	traci.simulationStep()
traci.close()
sys.exit()

第一步是启动traci.start, 里面给的几个参数, sumo-gui是指用这个命令来启动sumo的gui界面, 这一步要确认你在终端里面输入sumo-gui能出来sumo的gui界面, 才能继续;
然后下一个参数-c我也不知道啥意思;
下一个参数是sumo的cfg文件的位置, 里面提供了net, rou之类的文件信息;
然后emission这个是能耗的一些信息是我自己用的, 就不介绍了;

然后下面这一段就是让traci控制sumo开始一步步运行, 直到程序结束

while traci.simulation.getMinExpectedNumber() > 0:
	traci.simulationStep()

到什么时候结束呢? 大家应该注意到在sumo.cfg文件中有下面这样的设置:

    <time>
        <begin value="0"/>
        <end value="1000"/>
        <step-length value="0.1"/>
    </time>

所以应该是跑1000次, 然后0.1算一次, 也就是要跑1w个时间步长;
但是我实测发现, 如果地图里还有车没跑完, sumo就会继续跑, 不会受这里设置的时长限制;

然后下面介绍traci的一些其他功能, 比如我第一步想自己实现一个车辆的寻径功能, 也就是说, 在某一个时间点, 我要加一辆车到sumo里, 比如说叫"newcar", 然后用traci控制sumo设置这辆车的路线, 颜色, 速度之类的一系列信息;

第一步是获取当前路网的拓扑图, 也就是说我要先在python里构建一张图, 这里我用的是邻接表的方式创建图, 代码如下:
首先

net = sumolib.net.readNet("/Users/zhangpeiwen/Downloads/map/cmap/map.net.xml")
newVehicletype = 'evehicle'
AdjacencyList = generateTopology()

这个generateTopology方法如下:

def generateTopology():	
	AdjacencyList = {}
	for e in net.getEdges():
		if e.allows(newVehicletype)==False:
			continue;
		if AdjacencyList.__contains__(str(e.getFromNode().getID()))==False:
			AdjacencyList[str(e.getFromNode().getID())]={}
		AdjacencyList[str(e.getFromNode().getID())][str(e.getToNode().getID())] = e.getLanes()[0].getLength()
		
	return AdjacencyList;

这里先从net文件中读取所有的edges和nodes信息, 然后存进AdjacencyList里面, 这里注意我设置了一个newVehicletype, 因为道路是有自己的特点的, 不是所有类型的车都可以在任意道路上开, 我的研究对象是电动车, 所以我只考虑电动车能走的道路, 关于所有的vehicle class的信息大家可以去官网上找, 如下:
Abstract Vehicle Class

然后到这里为止我们就获得了一个AdjacencyList里面存着道路的拓扑图, 包括所有的道路, 节点信息;

在这里我想介绍一下sumo里的道路一些类的关系;

首先在traci for py里定义的一些类:

  • node
  • edge
  • junction
  • connection
  • lane

可以这么理解, 道路上的每一个路口都是一个node, 每条道路都是一个edge, 然后两个道路在某个点交汇会形成一个junction, 然后每一个node里可能会有很多的connection, 这个connection是用来连接两个edge的, 至于lane是车道的意思, 每一条edge里面都可能有多个lane, 一般节点的id长这样:601709881, 然后edge的id长这样-47228917#2, 这里的负号指的是方向, 可能存在一个edge和这条edge有一样的id, 就是负号不同, 然后后面#后面的数字指的就是车道, 就是lane编号;

然后下一步就是要把车辆加入到sumo中, 可以这么做:

def	addCar():
#	edge from
	ef = "27437516#2"
#	edge to
	et = "-38280723#0"
#	edge set
	es = generateRoute(ef, et)
	print(es)
	traci.route.add(routeID="newRoute", edges=es)
	traci.vehicle.add(routeID="newRoute", vehID="newCar", typeID="ElectricBus")
	traci.vehicle.setVehicleClass(vehID="newCar", clazz="evehicle")
	traci.vehicle.setEmissionClass(vehID="newCar", clazz="Energy/unknown")
	traci.vehicle.setColor(color=(0,255,0,238), vehID="newCar")
	pass;

代码意思应该都比较明显, 这里我的es = generateRoute(ef, et)方法是给定ef和et, 会返回一个集合, 包含了从ef到et经过的所有edge的集合;
这里其实有个问题, 如果我想随机的生成一组ef和et, 怎么来做呢?
如果随机从nodelist取出两个点肯定是不可以的, 因为道路中并不一定任意两点都可达, 所以我的做法如下, 有的时候还是会有bug, 大部分时候都是正常的:

	lenOfEdges = len(net.getEdges())
	while True:
		ef = net.getEdges()[random.randint(0,lenOfEdges-1)].getID()
		et = net.getEdges()[random.randint(0,lenOfEdges-1)].getID()
		if net.getEdge(ef).allows(newVehicletype)==False or net.getEdge(et).allows(newVehicletype)==False:
			continue;
		try:
			if len(traci.simulation.findRoute(fromEdge=ef, toEdge=et, vType="DEFAULT_VEHTYPE").edges)>0:
				break
		except:
			continue;
	print "ef is "+ef+"\net is "+et

其实本质是利用traci内置的方法traci.simulation.findRoute(fromEdge=ef, toEdge=et, vType="DEFAULT_VEHTYPE").edges去判断一下这两个点是不是可达的…这样的做法挺蠢的-.- 希望有大佬能给我提供一些更好的办法;

然后是寻找路径的办法, 这里可以直接用sumo提供的traci.simulation.findRoute(fromEdge=ef, toEdge=et, vType="DEFAULT_VEHTYPE").edges这个方法, 直接生成es路径, 在启动sumo的时候可以设置默认寻路器使用的算法:
traci.start(["sumo-gui", "-c", "/location/map.sumo.cfg", "--start", "false", "--routing-algorithm", "astar"], port=7911)
具体的可以在这里找到Routing Algorithms
默认提供了dijkstra, a*, ch和chwrapper四种方法

但是我这里是自己实现的, 我尝试了dijkstra, a*和best-first seach三种方法, 效果上看dijkstra效果还不错, 甚至表现比默认提供的dijkstra方法更好, 我猜测可能是sumo的默认实现的寻路考虑了一些别的东西, 比如道路车辆啊什么的, 这里我没有仔细去看源码取证;

这里放一个我实现的a*寻路:

def generateRoute(ef, et, INF=float("inf")):
	return generateMyRoute(ef, et)
	
def generateMyRoute(ef, et, INF=float("inf")):
	nf = str(net.getEdge(ef).getToNode().getID())
	nt = str(net.getEdge(et).getToNode().getID())
	print("nf is "+nf+"\nnt is "+nt)
	nodes = findNodeRoute(nf, nt)
	edges = []
	edges.append(ef)
	s = net.getEdge(ef).getLanes()[0].getLength()
	for i in range(0,len(nodes)-1):
		for x in net.getNode(nodes[i]).getOutgoing():
			if x.getToNode().getID()==nodes[i+1] and x.allows(newVehicletype):
				edges.append(x.getID())
				s+=x.getLanes()[0].getLength()
				break
	print len(nodes)
	print len(edges)
	print "dis is ",s
	return edges

def findNodeRoute(nf, nt, INF=float("inf")):
	openlist = {}
	closelist = {}
	openlist[nf] = [getF(nf, nt, 0), nf]
	while openlist.__contains__(nt)==False:
		u = -1
		minu = INF
		for x in openlist:
			if openlist[x][0]<minu :
				minu = openlist[x][0]
				u = x
		closelist[u] = openlist[u]
		del openlist[u]
		for x in AdjacencyList[u]:
			
			if closelist.__contains__(x)==False:
				if openlist.__contains__(x)==False:
					openlist[x] = [getF(x, nt, closelist[u][0]), u]
#				print(x+" is added into openlist")
				else:
					f = getF(x, nt, closelist[u][0])
					if f<openlist[x][0]:
						openlist[x] = [f, u]
				if x==nt:
					break
		if openlist.__contains__(nt):
			break
			
#	backtrace to find the route
	closelist[nt] = openlist[nt]
	del openlist[nt]
	u = nt
	nodes = []
	while u!=nf:
		nodes.insert(0, u)
		u = closelist[u][1]
	nodes.insert(0, nf)
	print "nodes are", nodes
	return nodes
	pass;


def getF(nf, nt, g):
	return getAF(nf, nt, g)
		
#A* Algoritym, f = h+g
def getAF(nf, nt, g):
	nfc = net.getNode(nf).getCoord()
	nft = net.getNode(nt).getCoord()
	return pow(pow(nfc[0]-nft[0], 2)+pow(nfc[1]-nft[1], 2), 0.5)+g
	
#Greedy Algorithm, Best-First Search
def getBF(nf, nt, g):
	nfc = net.getNode(nf).getCoord()
	nft = net.getNode(nt).getCoord()
	return pow(pow(nfc[0]-nft[0], 2)+pow(nfc[1]-nft[1], 2), 0.5)

上面的代码中, 核心A部分是findNodeRoute方法, 这里是通过A算法找到了最优路径走的所有的node, 返回一个nodelist, 然后通过generateMyRoute方法, 把找到的nodelist转换成edgelist, 返回给es作为路径参数;

然后这里的getF函数就是启发式函数, 我提供了两种, 一种是A*的, 也就是getAF, 一种是Greedy Algorithm, Best-First Search, 也就是getBF

到这里就基本能实现一个简单的寻路算法了;

还有一些我用到的其他方法,
比如traci.vehicle.getWaitingTime获取某个车辆在上一个step的等待时间;

比如traci.vehicle.getElectricityConsumption获取某个车辆在上一个step的能源消耗;

更多的方法都可以在官网和pydoc里面找到;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章