FastPlanner论文解读(一)——前端路径搜索

本系列将更新FastPlanner论文内容对应代码的解读,其github的工程位置如下:

GitCode - 全球开发者的开源社区,开源代码托管平台

一、摘要

在本文中,提出了一种稳健高效的四旋翼运动规划系统,主要架构如下:

1.kinodynamic 路径搜索方法在离散控制空间中找到一个安全的、kinodynamic 可行且时间最短初始轨迹。

2.我们通过 B 样条优化来改善轨迹的平滑度和间隙,该优化结合了来自欧几里得距离场 (EDF) 的梯度信息和动态约束,有效地利用了 B 样条的凸包特性。

3.将最终轨迹表示为非均匀 B 样条,采用迭代时间调整方法来保证动态可行和非保守轨迹。

前端采用Hybrid A*算法初步搜索出一条路径,进入后端使用软约束优化得到优化好的B样条曲线,再将其表示为非均匀B样条的方式,调整控制点和时间间隔。

二、前端

1.算法架构

前端的kinodynamic path searching使用hybrid A*算法,在地图中搜索出一条安全的、动力学的可行轨迹,算法流程如下:

2.图搜索算法

首先我们说明图搜索算法的思路:

图搜索算法本质上是在维护一个容器,里面装着未来可能访问的节点,开始为容器里面节点数量为0,然后放入第一个节点进行循环,在循环中:

1.把容器中最符合条件的节点找到并访问弹出

2.将这个节点对周围的节点进行扩展

3.将扩展的新节点放入容器中

同时,为了防止节点被反复循环,还需要另外一个容器装载被访问的节点,后面搜索时不能再进行访问。

在这个架构下我们分析经典的基于搜索的算法:

Dijkstra算法每次弹出容器中距离源节点代价最小的节点(距离源节点的代价我们使用g来表示)。

A*算法容器的代价函数记作f,其由两个部分组成,第一个部分是g,即从源节点到当前节点累计代价,第二个部分是h,h代表启发式Heuristic代价,f=g+h。对于h的衡量有多种选择,如L2距离、曼哈顿距离等。

在原本的A*算法下,hybrid A*不同于启发式函数和动力学可行kinodynamic。在这里采用离散输入量的办法,输入量为加速度a,则位置p可以表示为:

在a的范围内进行离散采样,根据初始位置和速度可以算出该采样输入a作用下转移的新状态,由此拓展出新的节点来,并且是动力学可行的。

3.代码架构

对于这里的算法结构

首先说明其中的变量含义

P为开集,即优先级队列,C为闭集,即已经进行扩展后的节点容器,nc为优先级队列中队首元素,nodes为拓展出的周围节点,primitives是离散输入作用的一段位移,gc为代价g,fc为总的代价f,整个流程为:

这里对应fast planner中的代码为:path_searching/src/kinodynamic_astar.cpp

cpp 复制代码
int KinodynamicAstar::search(Eigen::Vector3d start_pt, Eigen::Vector3d start_v, Eigen::Vector3d start_a,
                             Eigen::Vector3d end_pt, Eigen::Vector3d end_v, bool init, bool dynamic, double time_start)//A*搜索
{
  start_vel_ = start_v;
  start_acc_ = start_a;//设置起始点的速度和加速度

  PathNodePtr cur_node = path_node_pool_[0];//当前节点nc
  cur_node->parent = NULL;//父节点为空
  cur_node->state.head(3) = start_pt;//当前节点的位置
  cur_node->state.tail(3) = start_v;//当前节点的速度
  cur_node->index = posToIndex(start_pt);//将三维位置转化至栅格地图的index
  cur_node->g_score = 0.0;//当前节点的gc,即从起点到当前节点的cost,这里起点为0

  Eigen::VectorXd end_state(6);
  Eigen::Vector3i end_index;
  double time_to_goal;

  end_state.head(3) = end_pt;
  end_state.tail(3) = end_v;
  end_index = posToIndex(end_pt);
  cur_node->f_score = lambda_heu_ * estimateHeuristic(cur_node->state, end_state, time_to_goal);//计算启发式代价,初始点的fc总代价就是启发式代价,因为gc为0
  cur_node->node_state = IN_OPEN_SET;//当前节点的状态,一共两种,OPEN和CLOSED,在CLOSED中的节点是已经访问过的
  open_set_.push(cur_node);//OPEN加入当前节点,即优先级队列
  use_node_num_ += 1;

  if (dynamic)//为节点附加时间维度信息,动态搜索情况
  {
    time_origin_ = time_start;//记录起始时间
    cur_node->time = time_start;
    cur_node->time_idx = timeToIndex(time_start);//转换时间索引
    expanded_nodes_.insert(cur_node->index, cur_node->time_idx, cur_node);//将 (index, time_idx) 插入到 expanded_nodes_ 中,expanded_nodes_存储已访问节点的集合
    // cout << "time start: " << time_start << endl;//使用 (index, time_idx) 作为键值对,确保同一空间位置在不同时间点可以有不同的状态
  }
  else
    expanded_nodes_.insert(cur_node->index, cur_node);//否则将用空间位置索引来记录,静态情况

  PathNodePtr neighbor = NULL;
  PathNodePtr terminate_node = NULL;
  bool init_search = init;
  const int tolerance = ceil(1 / resolution_);

  while (!open_set_.empty())//主循环,优先级队列不为空
  {
    cur_node = open_set_.top();//弹出队首元素为当前节点nc

    // Terminate?
    bool reach_horizon = (cur_node->state.head(3) - start_pt).norm() >= horizon_;//接近边界
    bool near_end = abs(cur_node->index(0) - end_index(0)) <= tolerance &&
                    abs(cur_node->index(1) - end_index(1)) <= tolerance &&
                    abs(cur_node->index(2) - end_index(2)) <= tolerance;//解决目标节点

    if (reach_horizon || near_end)//如果某一个满足
    {
      terminate_node = cur_node;//当前节点为结束节点
      retrievePath(terminate_node);//从结束节点开始向后倒退得到路径
      if (near_end)
      {
        // Check whether shot traj exist
        estimateHeuristic(cur_node->state, end_state, time_to_goal);//计算启发式函数,主要得到optimal_time
        computeShotTraj(cur_node->state, end_state, time_to_goal);//根据这个optimal_time来计算能否直接生成一条动力学可行的路直接连接到目标点
        if (init_search)//如果是初始搜索,会报错,在初始搜索阶段,如果能够直接从起点生成一条连接到目标点的轨迹(shot trajectory),通常是不合理的
          ROS_ERROR("Shot in first search loop!");
      }
    }
    if (reach_horizon)//处理各种情况
    {
      if (is_shot_succ_)
      {
        std::cout << "reach end" << std::endl;
        return REACH_END;
      }
      else
      {
        std::cout << "reach horizon" << std::endl;
        return REACH_HORIZON;
      }
    }

    if (near_end)
    {
      if (is_shot_succ_)
      {
        std::cout << "reach end" << std::endl;
        return REACH_END;
      }
      else if (cur_node->parent != NULL)
      {
        std::cout << "near end" << std::endl;
        return NEAR_END;
      }
      else
      {
        std::cout << "no path" << std::endl;
        return NO_PATH;
      }
    }
    open_set_.pop();//弹出当前节点
    cur_node->node_state = IN_CLOSE_SET;//将当前节点加入CLOSED集合,已访问过的节点集合
    iter_num_ += 1;//迭代次数加1

    double res = 1 / 2.0, time_res = 1 / 1.0, time_res_init = 1 / 20.0;//输入进行离散化,r取2,则对应5个离散后的输入量,
    Eigen::Matrix<double, 6, 1> cur_state = cur_node->state;//当前的状态,位置和速度共6维
    Eigen::Matrix<double, 6, 1> pro_state;//扩展后的节点状态
    vector<PathNodePtr> tmp_expand_nodes;//临时存储当前节点扩展出的所有子节点
    Eigen::Vector3d um;//代表三个维度离散的控制量
    double pro_t;//表示扩展出的子节点的时间
    vector<Eigen::Vector3d> inputs;
    vector<double> durations;
    if (init_search)
    {
      inputs.push_back(start_acc_);
      for (double tau = time_res_init * init_max_tau_; tau <= init_max_tau_ + 1e-3;
           tau += time_res_init * init_max_tau_)
        durations.push_back(tau);//如果初次搜索,每次为的时间间隔三1/20,更加精细化的搜索
      init_search = false;
    }
    else
    {
      for (double ax = -max_acc_; ax <= max_acc_ + 1e-3; ax += max_acc_ * res)
        for (double ay = -max_acc_; ay <= max_acc_ + 1e-3; ay += max_acc_ * res)
          for (double az = -max_acc_; az <= max_acc_ + 1e-3; az += max_acc_ * res)//对输入进行离散化,每次加max/r,每个离散2r+1个量,一共有(2r+1)3种组合
          {
            um << ax, ay, az;
            inputs.push_back(um);
          }
      for (double tau = time_res * max_tau_; tau <= max_tau_; tau += time_res * max_tau_)//对时间进行离散化,控制输入的持续时间集合τ,有多种的时间选择(不只是t=0.5)使得新生成的路径更加多样化,不仅由离散的控制量决定,也可由离散的时间量决定
        durations.push_back(tau);//这里由于time_res为1/1.0,所以durations只有一个值max_tau_
    }

    // cout << "cur state:" << cur_state.head(3).transpose() << endl;
    for (int i = 0; i < inputs.size(); ++i)
      for (int j = 0; j < durations.size(); ++j)
      {
        um = inputs[i];
        double tau = durations[j];
        stateTransit(cur_state, pro_state, um, tau);//调用状态转移函数,控制量um和时间tao作用下,得到新的状态
        pro_t = cur_node->time + tau;//新状态的时间为nc当前节点的时间加上tau

        Eigen::Vector3d pro_pos = pro_state.head(3);//拓展后节点的位置

        // Check if in close set
        Eigen::Vector3i pro_id = posToIndex(pro_pos);//拓展后节点的栅格地图的index
        int pro_t_id = timeToIndex(pro_t);//拓展后节点的时间index
        PathNodePtr pro_node = dynamic ? expanded_nodes_.find(pro_id, pro_t_id) : expanded_nodes_.find(pro_id);//判断是动态搜索还是静态搜索,在expanded_nodes_添加不同的键值对
        if (pro_node != NULL && pro_node->node_state == IN_CLOSE_SET)//拓展出来的节点不是空节点,但其位于CLOSED集合内
        {
          if (init_search)
            std::cout << "close" << std::endl;
          continue;
        }

        // Check maximal velocity
        Eigen::Vector3d pro_v = pro_state.tail(3);//获取拓展后节点的速度
        if (fabs(pro_v(0)) > max_vel_ || fabs(pro_v(1)) > max_vel_ || fabs(pro_v(2)) > max_vel_)//检查新状态的速度是否超出最大速度限制
        {
          if (init_search)
            std::cout << "vel" << std::endl;
          continue;
        }

        // Check not in the same voxel
        Eigen::Vector3i diff = pro_id - cur_node->index;// 空间索引差值
        int diff_time = pro_t_id - cur_node->time_idx;// 时间索引差值
        if (diff.norm() == 0 && ((!dynamic) || diff_time == 0))//diff.norm() == 0在三维空间索引上完全相同,两个节点位于同一个空间体素中;((!dynamic) || diff_time == 0),静态搜索(!dynamic),不检查时间维度,动态搜索检查时间维度
        {
          if (init_search)
            std::cout << "same" << std::endl;
          continue;
        }

        // Check safety
        Eigen::Vector3d pos;
        Eigen::Matrix<double, 6, 1> xt;
        bool is_occ = false;
        for (int k = 1; k <= check_num_; ++k)
        {
          double dt = tau * double(k) / double(check_num_);//将时间tau分成很多小段
          stateTransit(cur_state, xt, um, dt);//每一个小段进行状态转移得到中间过程中的状态
          pos = xt.head(3);//获取中间状态的位置
          if (edt_environment_->sdf_map_->getInflateOccupancy(pos) == 1 )//检查中间状态是否在障碍物中,1表示在
          {
            is_occ = true;
            break;
          }
        }
        if (is_occ)//路径不安全
        {
          if (init_search)
            std::cout << "safe" << std::endl;
          continue;//跳过当前路径扩展
        }

        double time_to_goal, tmp_g_score, tmp_f_score;
        tmp_g_score = (um.squaredNorm() + w_time_) * tau + cur_node->g_score;//拓展后节点的gc值是其父节点的gc值加上从当前节点到拓展节点的cost,cost计算使用ec=(u^2+w_time_)*tau
        tmp_f_score = tmp_g_score + lambda_heu_ * estimateHeuristic(pro_state, end_state, time_to_goal);//拓展节点总的cost fc是gc+启发式代价

        // Compare nodes expanded from the same parent
        bool prune = false;//剪枝的标志位
        for (int j = 0; j < tmp_expand_nodes.size(); ++j)//遍历所有拓展节点
        {
          PathNodePtr expand_node = tmp_expand_nodes[j];
          if ((pro_id - expand_node->index).norm() == 0 && ((!dynamic) || pro_t_id == expand_node->time_idx))//检查空间上是否重复或动态模式下时间重复
          {
            prune = true;//标志位为true,标志需要剪枝
            if (tmp_f_score < expand_node->f_score)//判断fc是否比原来的小,如果更小将说明当前的路径更优
            {
              expand_node->f_score = tmp_f_score;//更新fc
              expand_node->g_score = tmp_g_score;//更新gc
              expand_node->state = pro_state;//状态量为拓展节点的状态量位置和速度
              expand_node->input = um;//节点的输入
              expand_node->duration = tau;//节点的持续时间
              if (dynamic)
                expand_node->time = cur_node->time + tau;//pro_state
            }
            break;
          }
        }

        // This node end up in a voxel different from others
        if (!prune)//只有在前面的剪枝检查中未被标记为冗余节点(prune == false)时,才进一步处理新扩展的节点
        {
          if (pro_node == NULL)//拓展节点pro_node,如果新节点 pro_node 尚未存在
          {
            pro_node = path_node_pool_[use_node_num_];//创建新节点
            pro_node->index = pro_id;
            pro_node->state = pro_state;
            pro_node->f_score = tmp_f_score;
            pro_node->g_score = tmp_g_score;
            pro_node->input = um;
            pro_node->duration = tau;
            pro_node->parent = cur_node;
            pro_node->node_state = IN_OPEN_SET;
            if (dynamic)
            {
              pro_node->time = cur_node->time + tau;
              pro_node->time_idx = timeToIndex(pro_node->time);
            }
            open_set_.push(pro_node);//将该节点加入open_set中

            if (dynamic)//记录一下节点的时间序列键值对
              expanded_nodes_.insert(pro_id, pro_node->time, pro_node);
            else
              expanded_nodes_.insert(pro_id, pro_node);

            tmp_expand_nodes.push_back(pro_node);//放在临时存储的节点

            use_node_num_ += 1;//内存检查
            if (use_node_num_ == allocate_num_)
            {
              cout << "run out of memory." << endl;
              return NO_PATH;
            }
          }
          else if (pro_node->node_state == IN_OPEN_SET)//若节点已经存在在open_set中
          {
            if (tmp_g_score < pro_node->g_score)//检查fc是否更小,若更优,则更新该节点的信息
            {
              // pro_node->index = pro_id;
              pro_node->state = pro_state;
              pro_node->f_score = tmp_f_score;
              pro_node->g_score = tmp_g_score;
              pro_node->input = um;
              pro_node->duration = tau;
              pro_node->parent = cur_node;
              if (dynamic)
                pro_node->time = cur_node->time + tau;
            }
          }
          else
          {
            cout << "error type in searching: " << pro_node->node_state << endl;//新节点的状态既不是 NULL,也不是 IN_OPEN_SET
          }
        }
      }
    // init_search = false;
  }

  cout << "open set empty, no path!" << endl;
  cout << "use node num: " << use_node_num_ << endl;
  cout << "iter num: " << iter_num_ << endl;
  return NO_PATH;
}

4.主要函数

1.核心函数

这里核心函数是三个,Expand()、EdgeCost()和Heuristic()

1.Expand()函数

由节点nc拓展生成小轨迹primitive

轨迹为p(t),由xyz三个轴的轨迹组成,轨迹由多项式来表示:

由微分平坦性可得:

这样得到系统的状态方程为:

得到最后状态方程的解为:

在这里输入量的范围是[−umax, umax],将其离散化为:

{−umax, − r−1 /r umax, · · · , r−1/r umax, umax},一共2r+1个离散量,三个维度一共产种组合,即动作量primitives。

对应的代码是:

cpp 复制代码
double res = 1 / 2.0, time_res = 1 / 1.0, time_res_init = 1 / 20.0;//输入进行离散化,r取2,则对应5个离散后的输入量,
    Eigen::Matrix<double, 6, 1> cur_state = cur_node->state;//当前的状态,位置和速度共6维
    Eigen::Matrix<double, 6, 1> pro_state;//扩展后的节点状态
    vector<PathNodePtr> tmp_expand_nodes;//临时存储当前节点扩展出的所有子节点
    Eigen::Vector3d um;//代表三个维度离散的控制量
    double pro_t;//表示扩展出的子节点的时间
    vector<Eigen::Vector3d> inputs;
    vector<double> durations;
    if (init_search)
    {
      inputs.push_back(start_acc_);
      for (double tau = time_res_init * init_max_tau_; tau <= init_max_tau_ + 1e-3;
           tau += time_res_init * init_max_tau_)
        durations.push_back(tau);//如果初次搜索,每次为的时间间隔是1/20,更加精细化的搜索
      init_search = false;
    }
    else
    {
      for (double ax = -max_acc_; ax <= max_acc_ + 1e-3; ax += max_acc_ * res)
        for (double ay = -max_acc_; ay <= max_acc_ + 1e-3; ay += max_acc_ * res)
          for (double az = -max_acc_; az <= max_acc_ + 1e-3; az += max_acc_ * res)//对输入进行离散化,每次加max/r,每个离散2r+1个量,一共有(2r+1)3种组合
          {
            um << ax, ay, az;
            inputs.push_back(um);
          }
      for (double tau = time_res * max_tau_; tau <= max_tau_; tau += time_res * max_tau_)//对时间进行离散化,控制输入的持续时间集合τ,有多种的时间选择(不只是t=0.5)使得新生成的路径更加多样化,不仅由离散的控制量决定,也可由离散的时间量决定
        durations.push_back(tau);//这里由于time_res为1/1.0,所以durations只有一个值max_tau_
    }

在这里获得了输入量inputs和持续时间durations,这里除了开始时按照1/20来得到tau,后面的tau都是max_tai_=0.5s,动作量被记录在inputs中。

2.EdgeCost函数

如何计算每段小轨迹的代价值

每段轨迹的cost计算函数为:

在这一小段时间τ内,认为离散输入量ud没有发生变化,所以近似其cost为

节点nc对应的gc是从源节点到当前节点的cost和的累计值,因此当前节点的gc为:

3.Heuristic函数

评估一个节点的启发代价值

这里要利用庞特里亚金极小值原理求解:

系统的状态为

系统状态方程为

cost为:

构造哈密尔顿函数:

由极值存在的必要条件可得:

得出:

由必要条件可得:

,输入量为a,

因此将H对a求导数可得:

得到:

由a积分得到v和p,再带入T可得:

此时J*是T的函数,求出得到根Th,则为启发式代价hc,总代价就为:

此功能在函数estimateHeuristic中实现:这里求根是用的matlab中的特征根法,传入的参数是起点和终点的位置和速度

cpp 复制代码
double KinodynamicAstar::estimateHeuristic(Eigen::VectorXd x1, Eigen::VectorXd x2, double& optimal_time)//计算启发式代价,即用庞特里亚金极小值原理求解的最小代价
{//x1是起始点的位置和速度向量,共六维,x2是目标点的位置和速度向量,6维
  const Vector3d dp = x2.head(3) - x1.head(3);//位置差值,即pug-puc
  const Vector3d v0 = x1.segment(3, 3);//起始点的速度
  const Vector3d v1 = x2.segment(3, 3);//目标点的速度

  double c1 = -36 * dp.dot(dp);//这几个系数是对J(T)函数求导后的多项式系数
  double c2 = 24 * (v0 + v1).dot(dp);
  double c3 = -4 * (v0.dot(v0) + v0.dot(v1) + v1.dot(v1));
  double c4 = 0;
  double c5 = w_time_;//即公式中的p(rou)

  std::vector<double> ts = quartic(c5, c4, c3, c2, c1);//特征根法求解导数方程的解,得到根T,这里是一个数组ts

  double v_max = max_vel_ * 0.5;
  double t_bar = (x1.head(3) - x2.head(3)).lpNorm<Infinity>() / v_max;
  ts.push_back(t_bar);//估算一个最小值,用dp中最大的一个维度除以最大速度的一半,用于后续进行比较

  double cost = 100000000;//记录最小的cost,初始赋值为一个很大的数
  double t_d = t_bar;//用于记录最小cost对应的根

  for (auto t : ts)//便利所有的解
  {
    if (t < t_bar)
      continue;
    double c = -c1 / (3 * t * t * t) - c2 / (2 * t * t) - c3 / t + w_time_ * t;//J(T)函数,计算cost
    if (c < cost)
    {
      cost = c;
      t_d = t;
    }
  }

  optimal_time = t_d;//容器optimal_time中记录了最优的时间Th

  return 1.0 * (1 + tie_breaker_) * cost;//加入一个微小的偏移量,用于打破相同代价的情况下的平局
}

2.其他函数

同时,还有以下几个函数值得注意:

1.AnalyticExpand()

由于离散化的 control input,很难在 goal state 中有一个 primitive end,即我们在nc点通过离散输入的作用,下一个状态刚好到达了goal点。为了补偿它并加快搜索速度,我们引入了一个分析扩展方案。

主要思路是,我们弹出节点nc,用之前相同的方法算一条从当前点nc到目标点xg的路径,如果这条路径是安全的(没有障碍物碰撞)并且是动力学可行的,那么我们就提前停止搜索。

对应的代码是这一段:

cpp 复制代码
// Terminate?
    bool reach_horizon = (cur_node->state.head(3) - start_pt).norm() >= horizon_;//接近边界
    bool near_end = abs(cur_node->index(0) - end_index(0)) <= tolerance &&
                    abs(cur_node->index(1) - end_index(1)) <= tolerance &&
                    abs(cur_node->index(2) - end_index(2)) <= tolerance;//解决目标节点

    if (reach_horizon || near_end)//如果某一个满足
    {
      terminate_node = cur_node;//当前节点为结束节点
      retrievePath(terminate_node);//从结束节点开始向后倒退得到路径
      if (near_end)
      {
        // Check whether shot traj exist
        estimateHeuristic(cur_node->state, end_state, time_to_goal);//计算启发式函数,主要得到optimal_time
        computeShotTraj(cur_node->state, end_state, time_to_goal);//根据这个optimal_time来计算能否直接生成一条动力学可行的路直接连接到目标点
        if (init_search)//如果是初始搜索,会报错,在初始搜索阶段,如果能够直接从起点生成一条连接到目标点的轨迹(shot trajectory),通常是不合理的
          ROS_ERROR("Shot in first search loop!");
      }
    }
    if (reach_horizon)//处理各种情况
    {
      if (is_shot_succ_)
      {
        std::cout << "reach end" << std::endl;
        return REACH_END;
      }
      else
      {
        std::cout << "reach horizon" << std::endl;
        return REACH_HORIZON;
      }
    }

    if (near_end)
    {
      if (is_shot_succ_)
      {
        std::cout << "reach end" << std::endl;
        return REACH_END;
      }
      else if (cur_node->parent != NULL)
      {
        std::cout << "near end" << std::endl;
        return NEAR_END;
      }
      else
      {
        std::cout << "no path" << std::endl;
        return NO_PATH;
      }
    }

首先检查是否接近目标点,使用标识位near_end,计算启发式代价得到最优时间Th,通过这个Th计算从当前点到终点直接连线的路径,具体是在函数computeShort内完成的。

cpp 复制代码
bool KinodynamicAstar::computeShotTraj(Eigen::VectorXd state1, Eigen::VectorXd state2, double time_to_goal)//计算一个从当前节点到目标节点的直接路径
{
  /* ---------- get coefficient ---------- */
  const Vector3d p0 = state1.head(3);
  const Vector3d dp = state2.head(3) - p0;
  const Vector3d v0 = state1.segment(3, 3);
  const Vector3d v1 = state2.segment(3, 3);
  const Vector3d dv = v1 - v0;
  double t_d = time_to_goal;
  MatrixXd coef(3, 4);//三维轨迹的多项式系数存储为矩阵
  end_vel_ = v1;

  Vector3d a = 1.0 / 6.0 * (-12.0 / (t_d * t_d * t_d) * (dp - v0 * t_d) + 6 / (t_d * t_d) * dv);
  Vector3d b = 0.5 * (6.0 / (t_d * t_d) * (dp - v0 * t_d) - 2 / t_d * dv);
  Vector3d c = v0;
  Vector3d d = p0;

  // 1/6 * alpha * t^3 + 1/2 * beta * t^2 + v0  使用三次多项式描述轨迹p(t)
  // a*t^3 + b*t^2 + v0*t + p0   a,b,c,d多项是
  coef.col(3) = a, coef.col(2) = b, coef.col(1) = c, coef.col(0) = d;//对应每一列是ax,ay,az;bx,by,bz等

  Vector3d coord, vel, acc;
  VectorXd poly1d, t, polyv, polya;
  Vector3i index;

  Eigen::MatrixXd Tm(4, 4);
  Tm << 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, 0, 0, 0;//用于求导数的

  /* ---------- forward checking of trajectory ---------- */
  double t_delta = t_d / 10;//td是optimal_time,将其分为一段一段,对每个时间点t,计算对应的轨迹位置、速度、加速度,并进行检查
  for (double time = t_delta; time <= t_d; time += t_delta)
  {
    t = VectorXd::Zero(4);//t是一个多项式,表示[1,t,t2,t3]多项式
    for (int j = 0; j < 4; j++)
      t(j) = pow(time, j);

    for (int dim = 0; dim < 3; dim++)
    {
      poly1d = coef.row(dim);//从0-2行的系数,对应x,y,z
      coord(dim) = poly1d.dot(t);//计算位置
      vel(dim) = (Tm * poly1d).dot(t);//计算速度
      acc(dim) = (Tm * Tm * poly1d).dot(t);//计算加速度

      if (fabs(vel(dim)) > max_vel_ || fabs(acc(dim)) > max_acc_)//检查速度和加速度是否超限
      {
        // cout << "vel:" << vel(dim) << ", acc:" << acc(dim) << endl;
        // return false;
      }
    }

    if (coord(0) < origin_(0) || coord(0) >= map_size_3d_(0) || coord(1) < origin_(1) || coord(1) >= map_size_3d_(1) ||
        coord(2) < origin_(2) || coord(2) >= map_size_3d_(2))//检查轨迹是否超出地图边界
    {
      return false;
    }

    // if (edt_environment_->evaluateCoarseEDT(coord, -1.0) <= margin_) {
    //   return false;
    // }
    if (edt_environment_->sdf_map_->getInflateOccupancy(coord) == 1)//检查轨迹位置是否碰撞障碍物,碰到障碍物则无法生成
    {
      return false;
    }
  }
  //轨迹生成结果
  coef_shot_ = coef;//系数
  t_shot_ = t_d;//最优时间
  is_shot_succ_ = true;//能否生成直接连接轨迹标志位
  return true;
}

传入的参数是起点和终点的状态,包括位置和速度,同时传入轨迹的时间,是之前利用极小值原理计算出来的最优时间。

系数存放在coef内,三列对应xyz三个轴,4代表多项式系数,对应多项式为:

t从t_delta开始每次增加t_delta,在每个时间点检查速度、加速度是否动力学可行,同时检查位置是否在地图范围内和是否有障碍物,最终返回生成轨迹成功与否。

2.Prune()函数

计算出根据动作量primitives拓展出来的节点nodes,并进行剪枝操作留下复合要求的节点。

对应代码为:

cpp 复制代码
// Check safety
        Eigen::Vector3d pos;
        Eigen::Matrix<double, 6, 1> xt;
        bool is_occ = false;
        for (int k = 1; k <= check_num_; ++k)
        {
          double dt = tau * double(k) / double(check_num_);//将时间tau分成很多小段
          stateTransit(cur_state, xt, um, dt);//每一个小段进行状态转移得到中间过程中的状态
          pos = xt.head(3);//获取中间状态的位置
          if (edt_environment_->sdf_map_->getInflateOccupancy(pos) == 1 )//检查中间状态是否在障碍物中,1表示在
          {
            is_occ = true;
            break;
          }
        }
        if (is_occ)//路径不安全
        {
          if (init_search)
            std::cout << "safe" << std::endl;
          continue;//跳过当前路径扩展
        }

        double time_to_goal, tmp_g_score, tmp_f_score;
        tmp_g_score = (um.squaredNorm() + w_time_) * tau + cur_node->g_score;//拓展后节点的gc值是其父节点的gc值加上从当前节点到拓展节点的cost,cost计算使用ec=(u^2+w_time_)*tau
        tmp_f_score = tmp_g_score + lambda_heu_ * estimateHeuristic(pro_state, end_state, time_to_goal);//拓展节点总的cost fc是gc+启发式代价

        // Compare nodes expanded from the same parent
        bool prune = false;//剪枝的标志位
        for (int j = 0; j < tmp_expand_nodes.size(); ++j)//遍历所有拓展节点
        {
          PathNodePtr expand_node = tmp_expand_nodes[j];
          if ((pro_id - expand_node->index).norm() == 0 && ((!dynamic) || pro_t_id == expand_node->time_idx))//检查空间上是否重复或动态模式下时间重复
          {
            prune = true;//标志位为true,标志需要剪枝
            if (tmp_f_score < expand_node->f_score)//判断fc是否比原来的小,如果更小将说明当前的路径更优
            {
              expand_node->f_score = tmp_f_score;//更新fc
              expand_node->g_score = tmp_g_score;//更新gc
              expand_node->state = pro_state;//状态量为拓展节点的状态量位置和速度
              expand_node->input = um;//节点的输入
              expand_node->duration = tau;//节点的持续时间
              if (dynamic)
                expand_node->time = cur_node->time + tau;//pro_state
            }
            break;
          }
        }

首先对通过primitives到目标点的路径进行障碍物检查,这里用的是采样的方法,在路径中间一小段时间进行采样,检查其是否位于障碍物内。

对通过安全性检查的节点进行剪枝操作,如果检测到当前扩展的节点与已有节点相同(位置和时间),并且当前路径的总代价更小,则更新节点的 f_scoreg_scorestateinputduration 等信息并继续。

3.扩展新的节点

接着我们来看如何进行路径扩展 中的新节点创建和更新

对应代码如下:

cpp 复制代码
// This node end up in a voxel different from others
        if (!prune)//只有在前面的剪枝检查中未被标记为冗余节点(prune == false)时,才进一步处理新扩展的节点
        {
          if (pro_node == NULL)//拓展节点pro_node,如果新节点 pro_node 尚未存在
          {
            pro_node = path_node_pool_[use_node_num_];//创建新节点
            pro_node->index = pro_id;
            pro_node->state = pro_state;
            pro_node->f_score = tmp_f_score;
            pro_node->g_score = tmp_g_score;
            pro_node->input = um;
            pro_node->duration = tau;
            pro_node->parent = cur_node;
            pro_node->node_state = IN_OPEN_SET;
            if (dynamic)
            {
              pro_node->time = cur_node->time + tau;
              pro_node->time_idx = timeToIndex(pro_node->time);
            }
            open_set_.push(pro_node);//将该节点加入open_set中

            if (dynamic)//记录一下节点的时间序列键值对
              expanded_nodes_.insert(pro_id, pro_node->time, pro_node);
            else
              expanded_nodes_.insert(pro_id, pro_node);

            tmp_expand_nodes.push_back(pro_node);//放在临时存储的节点

            use_node_num_ += 1;//内存检查
            if (use_node_num_ == allocate_num_)
            {
              cout << "run out of memory." << endl;
              return NO_PATH;
            }
          }
          else if (pro_node->node_state == IN_OPEN_SET)//若节点已经存在在open_set中
          {
            if (tmp_g_score < pro_node->g_score)//检查fc是否更小,若更优,则更新该节点的信息
            {
              // pro_node->index = pro_id;
              pro_node->state = pro_state;
              pro_node->f_score = tmp_f_score;
              pro_node->g_score = tmp_g_score;
              pro_node->input = um;
              pro_node->duration = tau;
              pro_node->parent = cur_node;
              if (dynamic)
                pro_node->time = cur_node->time + tau;
            }
          }
          else
          {
            cout << "error type in searching: " << pro_node->node_state << endl;//新节点的状态既不是 NULL,也不是 IN_OPEN_SET
          }
        }

其代码的架构为:

cpp 复制代码
if (!prune)
{
  if (pro_node == NULL)
  {
    // 创建新节点,表示如果当前扩展的节点还不存在(即是一个新节点),则需要创建一个新的节点。
  }
  else if (pro_node->node_state == IN_OPEN_SET)
  {
    // 如果节点已存在并在OPEN集合中,检查路径是否更优
  }
  else
  {
    std::cout << "error type in searching: " << pro_node->node_state << std::endl;
  }
}

第一段代码将扩展的节点加入到OPEN集合中

核心在于第二段,从OPEN集合中找到一个fc最优的节点,将当前节点设置为该节点的父节点,并更新其各个代价信息等。

对应这一段功能

总体的代码流程如下:

cpp 复制代码
1. 初始化
输入:起始点位置、速度、加速度,目标点位置、速度。
设置起始节点:
设定起始点的 位置、速度、加速度。
计算起始节点的 代价(g_score) 和 启发式代价(f_score),并加入 开放集(open_set)。
2. 主循环:处理开放集中的节点
判断开放集是否为空:
如果为空,算法结束,返回 NO_PATH。
如果非空,弹出 代价最小的节点 cur_node。
3. 检查终止条件
接近边界:检查当前节点是否接近预定的边界(例如搜索区域的最大范围),若是,设置标志为 reach_horizon。
接近目标节点:检查当前节点是否接近目标节点,若是,设置标志为 near_end。
如果满足终止条件:
从目标节点开始倒退,找到路径并返回 REACH_END。
如果接近边界但未到达目标,返回 REACH_HORIZON。
4. 路径扩展
对于每个可能的 控制输入 和 持续时间,计算新的状态(位置、速度等)。
计算扩展节点的代价:计算从当前节点到扩展节点的代价(g_score 和 f_score)。
5. 剪枝操作
检查扩展的节点是否已经处理过:
如果扩展的节点已经在 CLOSED 集合 中,跳过该节点。
如果扩展的节点已经在 OPEN 集合 中,并且当前路径代价更优,则更新该节点。
6. 安全性检查
将路径分成小段,检查每个小段是否碰到障碍物:
如果路径中的任意小段与障碍物碰撞,则标记该路径为 不安全,跳过该路径扩展。
7. 节点更新
新节点创建:如果扩展的节点是新的,创建该节点并加入 OPEN 集合,同时更新节点的状态和代价。
更新已存在的节点:如果节点已经在 OPEN 集合 中,并且当前路径更优,则更新该节点的信息(状态、代价等)。
8. 内存管理
检查节点是否超出预定的最大数量。如果超出,则返回 NO_PATH,表示内存不足。
9. 返回结果
如果找到路径,返回 REACH_END。
如果没有路径可扩展,返回 NO_PATH。
结束条件:
终止条件检查、路径扩展、剪枝、安全性检查等操作反复进行,直到找到路径或者无法找到路径为止。
相关推荐
荣--10 分钟前
C++学习:CRTP 模式是什么
c++·设计模式·模板类
CodeClimb21 分钟前
【华为OD-E卷 - 任务最优调度 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
近听水无声47721 分钟前
c++模板进阶
c++·学习
axxy200022 分钟前
C++ Primer Plus第六章课后习题总结
数据结构·c++·算法
锐策28 分钟前
『 C++ 』中理解回调类型在 C++ 中的使用方式。
开发语言·c++
利刃大大36 分钟前
【回溯+剪枝】组合问题!
c++·算法·深度优先·剪枝
_extraordinary_1 小时前
C++哈希(链地址法)(二)详解
c++·算法·哈希算法
进击ing小白1 小时前
c++可变参数详解
开发语言·c++
_extraordinary_2 小时前
C++11详解(一) -- 列表初始化,右值引用和移动语义
开发语言·c++·c++11
0xCC说逆向2 小时前
Windows图形界面(GUI)-QT-C/C++ - QT Frame
c语言·开发语言·c++·windows·qt