4.A星算法主程序 algorithm.cpp
4.1 几个算法相关的重要的概念
(1)状态空间
- R3:三维状态空间,包括:( x , y , θ x,y,\theta x,y,θ)
- R2:二维状态空间,包括:( x , y x,y x,y)
- R4:四维状态空间,包括:( x , y , θ , r x,y,\theta,r x,y,θ,r), r r r代表前进方向,1为前,0为后.
(2)节点
A星搜索的每个状态都是一个节点,算法中用到了两种类型:
- 3D节点:程序中表示为Node3D类型,主要包含( x , y , θ x,y,\theta x,y,θ)三种状态.
- 2D节点:程序中表示为Node2D类型,主要包括( x , y x,y x,y)两种状态.
2D节点就是传统A星所使用的节点,主要是为了加入障碍物约束.
3D节点是基于RR曲线生成的,是为了加入动力学约束.
(3)表
在A星搜索的过程中,有两种类型的表open表
和closed表
,拓展过的点会加入到closed表
中,未被拓展过的节点加入open表
中.
4.2 (核心)Algorithm::hybridAStar
关于Algorithm::hybridAStar函数的输入和返回参数,在algorithm.h中,有详细的说明.
这里首先分析算法处理流程:
4.2.1 初始化变量
- 定义前任
iPred
和后继iSucc
; - 定义方向的数量:如果是考虑掉头的话,方向就是
6
个,不考虑掉头,方向就是3
个. - 迭代计数
iterations
:0
// PREDECESSOR AND SUCCESSOR INDEX
int iPred, iSucc;
float newG;
// Number of possible directions, 3 for forward driving and an additional 3 for reversing
int dir = Constants::reverse ? 6 : 3;
// Number of iterations the algorithm has run for stopping based on Constants::iterations
int iterations = 0;
4.2.2 设置一个二项堆BH来存放数据
关于二项堆的概念,可看这个博客:参考博客
关于二项堆用于排序的程序案例,可以看这个博客:参考博客.
在此处,二相堆在源代码中设置成了变量0,我觉得不太好辨认,就改称了BH.
此处BH的用途是:存放节点,并且按照C值的大小进行自动排序.本算法中,是按照C值升序排列.便于获取BH中节点的C值的最小值.
// OPEN LIST AS BOOST IMPLEMENTATION
typedef boost::heap::binomial_heap<Node3D*,boost::heap::compare<CompareNodes>> priorityQueue;
priorityQueue BH;
BH中使用的比较结构是CompareNodes,具体的含义是根据节点的C值升序排列.
struct CompareNodes {
bool operator()(const Node3D* lhs, const Node3D* rhs) const {
return lhs->getC() > rhs->getC();
}
bool operator()(const Node2D* lhs, const Node2D* rhs) const {
return lhs->getC() > rhs->getC();
}
};
4.2.3 初始化h值,BH的初始值以及节点指针
- 更新启发式
H
的值,H
代表到达goal
的预估值. - 把
start
节点加入open
表. - 把
start
节点的指针存在BH中. - 把前任节点的索引值
iPred
设置为start
节点的索引值. - 设置
nodes3D
的对应位置的元素为start
. - 定义指向前任节点指针为
nPred
,后继节点指针为nSucc
// update h value
updateH(start, goal, nodes2D, dubinsLookup, width, height, configurationSpace, visualization);
// mark start as open
start.open();
// push on priority queue aka open list
BH.push(&start);
iPred = start.setIdx(width, height);
nodes3D[iPred] = start;
// NODE POINTER
Node3D* nPred;
Node3D* nSucc; nSucc;
- 关于索引值的计算公式(来自
setIdx()
函数):
f i d x = ( i n t ) 72 t / ( 2 π ) ∗ w ∗ h + ( i n t ) y ∗ h + ( i n t ) x f_{idx}=(int)72t/(2\pi)*w*h+(int)y*h+(int)x fidx=(int)72t/(2π)∗w∗h+(int)y∗h+(int)x
其中,h
为地图高度,w
为地图宽度,x,y,t
为start
的坐标.
至于为什么取这个值,笔者也没有搞明白,留下一个疑问.
nodes3D
的容量设置为:
f i d x M a x = h ∗ w ∗ 72 f_{idxMax}=h*w*72 fidxMax=h∗w∗72
注意到当t趋近于 2 π 2\pi 2π时, f i d x f_{idx} fidx的第一项就已经接近 f i d x M a x f_{idxMax} fidxMax了.此处本想证明一下 f i d x M a x f_{idxMax} fidxMax一定大于等于 f i d x f_{idx} fidx.但是最终也没有数学证明出来,只是人工采点做了个实验:
案例:如果map
尺寸为h=50
,w=80
,那么:
f
i
d
x
M
a
x
=
288000
f_{idxMax}=288000
fidxMax=288000
此时,选start
点为(79.8,49.65,6.24)
,此时start
点位于地图的右上角,并且朝向接近于
2
π
2\pi
2π,计算出来的
f
i
d
x
f_{idx}
fidx值为:
f
i
d
x
=
287999
f_{idx}=287999
fidx=287999
实验似乎证明了索引值最大也不会超过
f
i
d
x
M
a
x
f_{idxMax}
fidxMax.
- 关于
start
坐标和goal
坐标的取值范围
x : [ 0 , w ] y : [ 0 , h ] t : [ 0 , 2 p i ) x:[0,w] y:[0,h] t:[0,2pi) x:[0,w]y:[0,h]t:[0,2pi)
4.2.4 操作BH,寻找goal节点
只要BH
不为空,就一直循环进行4.2.4
的操作,直到BH
为空:
(1)取出最小代价值BH.top()
作为前任节点nPred
,并且设置该节点的索引值为前任节点的索引值iPred
.
// pop node with lowest cost from priority queue
nPred = BH.top();
// set index
iPred = nPred->setIdx(width, height);
iterations++;
(2)判断前任节点nPred
是否已经在closed
表中了,如果是,则从BH
表中弹出,然后直接进行下一个循环.
// _____________________________
// LAZY DELETION of rewired node
// if there exists a pointer this node has already been expanded
if (nodes3D[iPred].isClosed()) {
// pop node from the open list and start with a fresh node
BH.pop();
continue;
}
(3)如果取出来的点不在closed
表中,那么就进行下面的流程:
(3.1)把当前的节点添加到closed
表中,然后从BH
中弹出.
// add node to closed list
nodes3D[iPred].close();
// remove node from open list
BH.pop();
(3.2)测试是否是目标节点.
如果是目标节点,并且叠代次数iterations
大于Constants::iterations
(可设置为0
),则直接返回nPred
作为目标节点,退出循环4.2.4.
// _________
// GOAL TEST
if (*nPred == goal || iterations > Constants::iterations) {
// DEBUG
return nPred;
}
(3.3)如果不是目标节点,那么进行以下流程:
(3.3.1)如果同时满足以下3个条件:(a)配置参数使能 (b)nPred
在goal
节点的 10m
范围内 ©nPred
向车辆的前方规划.那么进行以下流程:
- 根据杜宾斯发射函数
dubinsShot
生成后继节点nSucc
. - 如果
nSucc
不为空,并且等于goal
,那么返回nSucc
,跳出循环4.2.4
// _______________________
// SEARCH WITH DUBINS SHOT
if (Constants::dubinsShot && nPred->isInRange(goal) && nPred->getPrim() < 3) {
nSucc = dubinsShot(*nPred, goal, configurationSpace);
if (nSucc != nullptr && *nSucc == goal) {
//DEBUG
// std::cout << "max diff " << max << std::endl;
return nSucc;
}
}
(3.3.2)遍历6个方向
的后继节点,此处考虑倒车规划,所以dir=6
.
for (int i = 0; i < dir; i++) {
运行(3.3.2.1)-(3.3.2.2)
}
(3.3.2.1)首先生成后继节点createSuccessor(i)
并更新索引值
// create possible successor
nSucc = nPred->createSuccessor(i);
// set index of the successor
iSucc = nSucc->setIdx(width, height);
(3.3.2.2)而后确保nSucc
在栅格上isOnGrid
,并且是可访问的isTraversable
,并且节点不在closed
表上或者nSucc
和iPred
的索引相同.
isTraversable
主要是根据该节点占用的栅格是否存在障碍物来判断.
if (nSucc->isOnGrid(width, height) && configurationSpace.isTraversable(nSucc)) {
// ensure successor is not on closed list or it has the same index as the predecessor
if (!nodes3D[iSucc].isClosed() || iPred == iSucc) {
(3.3.2.2.1)
}else { delete nSucc; }
else { delete nSucc; }
(3.3.2.2.1)计算移动代价值
// calculate new G value
nSucc->updateG();
newG = nSucc->getG();
如果nodes3D[iSucc]
不在open
表里面,或者iSucc
和iPred
的索引值相同,或者后继节点的移动代价小于之前的移动代价:
// if successor not on open list or found a shorter way to the cell
if (!nodes3D[iSucc].isOpen() || newG < nodes3D[iSucc].getG() || iPred == iSucc) {
{ 运行(3.3.2.2.1.1)}
else { delete nSucc; }
(3.3.2.2.1.1)使用新的后继节点更新预估值H.
如果后继和前任是同一个,并且后继的C值比前任的C值大,那么就意味着后继没有价值,所以删除后继delete nSucc
,返回(3.2.2)
判断下一个方向的后继.
如果后继和前任是同一个,并且后继的C值比前任的C值小,那么就意味着新的后继价值更高,所以设置后继的前任为前任的前任nSucc->setPred(nPred->getPred());
如果后继的前任等于该后继本身,那么搜索树中存在环了.输出错误语句looping
.
最后,将后继加入open
表中,并更新后继节点向量,将该节点添加到BH
中,然后删除临时变量nSucc
,返回(3.2.2)
判断下一个方向的后继.
// calculate H value
updateH(*nSucc, goal, nodes2D, dubinsLookup, width, height, configurationSpace, visualization);
// if the successor is in the same cell but the C value is larger
if (iPred == iSucc && nSucc->getC() > nPred->getC() + Constants::tieBreaker) {
delete nSucc;
continue;
}
// if successor is in the same cell and the C value is lower, set predecessor to predecessor of predecessor
else if (iPred == iSucc && nSucc->getC() <= nPred->getC() + Constants::tieBreaker) {
nSucc->setPred(nPred->getPred());
}
if (nSucc->getPred() == nSucc) {
std::cout << "looping";
}
// put successor on open list
nSucc->open();
nodes3D[iSucc] = *nSucc;
BH.push(&nodes3D[iSucc]);
delete nSucc;
附1:Dubinsshot()
(1)初始化路径和点
// start
double q0[] = { start.getX(), start.getY(), start.getT() };
// goal
double q1[] = { goal.getX(), goal.getY(), goal.getT() };
// initialize the path
DubinsPath path;
// calculate the path
dubins_init(q0, q1, Constants::r, &path);
获取Dubins
曲线长度,并初始化Dubins
节点向量
int i = 0;
float x = 0.f;
float length = dubins_path_length(&path);
Node3D* dubinsNodes = new Node3D [(int)(length / Constants::dubinsStepSize) + 1];
Dubins
节点的长度设置为:(int)(length / Constants::dubinsStepSize) + 1
(2)遍历每一个Dubins
节点
(2.1)障碍物检测函数isTraversable
其中关键函数是isTraversable
函数中的configurationTest
函数,它位于collisiondetection.cpp
中,作用是检测某个目标点(x,y,t)是否有障碍物冲突,如果返回true,说明没有冲突.isTraversable
也会返回true.
while (x < length) {
double q[3];
dubins_path_sample(&path, x, q);
dubinsNodes[i].setX(q[0]);
dubinsNodes[i].setY(q[1]);
dubinsNodes[i].setT(Helper::normalizeHeadingRad(q[2]));
// collision check
if (configurationSpace.isTraversable(&dubinsNodes[i])) {
// set the predecessor to the previous step
if (i > 0) {
dubinsNodes[i].setPred(&dubinsNodes[i - 1]);
} else {
dubinsNodes[i].setPred(&start);
}
if (&dubinsNodes[i] == dubinsNodes[i].getPred()) {
std::cout << "looping shot";
}
x += Constants::dubinsStepSize;
i++;
} else {
// std::cout << "Dubins shot collided, discarding the path" << "\n";
// delete all nodes
delete [] dubinsNodes;
return nullptr;
}
}
(3)如果能顺利遍历完毕而没有return
,说明生成的杜宾斯曲线上所有的节点都是无碰撞的,或者说都是可通过的,那么就返回最后一个节点
// std::cout << "Dubins shot connected, returning the path" << "\n";
return &dubinsNodes[i - 1];
附2:关于点的prim属性
node3d.cpp->Line66:
return new Node3D(xSucc, ySucc, tSucc, g, 0, this, i);
其中,i表示的是拓展的第i个后继节点.如果考虑不倒车,i的取值范围是(0,2),如果考虑倒车,那么i的取值范围是(0,5).