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裏面找到;

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