路径规划[2/4]:RRTConnect::solve

  • solve总逻辑。进行一轮轮迭代,在每轮迭代,1)生成一个随机结点rmotion。2)调用growTree,以rmotion为新采样点,对初始树进行扩展。3)确保rmotion结点值等于新添加到初始树的结点值。3)不断扩展目标树,直到或扩展到了rmotion,意味着找到路径,解决;或遇到了碰撞冲突、无法再扩展,进入下一轮迭代。
  • 系统存在两棵树:tStart_、tGoal_。在每轮迭代,是tStart_还是tGoal_作为初始树,是交替的。换句话说,第一轮时tStart_是初始树,tGoal_是目标树。到第二轮,换作tGoal_是初始树,tStart_是目标树。第三轮和第一轮一样,依此类推。
  • 在每一轮迭代,初始树只会扩展一次,目标树可能多次。
  • rmotion这个结点的root、parent总是nullptr,和它同值(state字段)的结点会存在树中,但它不会存在任何树。
  • RRTConnect::solve后得到的path_不是client希望的规划轨迹,后续需要依次调用simplifySolution、interpolateSolution进行转换。

 

一、一个运行示例

  • 机械臂有a、b、c三个关节,都是单自由度。
  • 初始状态:(-0.56, 0.79, -0.45)
  • 目标状态:(1, 2, -1)
图1 RRTConnect::solve示例

以下是示例的执行过程。

  1. 初始结点(-0.56, 0.79, -0.45)加入初始树,树中结点数1。进入第1轮迭代。
  2. sampleUniform随机生成一个采样值,值更新到rmotion(-0.87008, 2.72230, 0.23816)。
  3. [第一次growTree]nearest(rmotion)找到离rmotion最近的树中结点(-0.56, 0.79, -0.45),记为nmotion。
  4. [growTree]算出nmotion和rmotion两结点距离是2.93054,大于扩展阀值1.884。于是以nmotion为起点,最大可扩展距离1.884为增加值,生成新采样点(-0.75934, 2.03224, -0.00759),记为dstate。
  5. [growTree]checkMotion(dstate)进行碰撞检测,结果是没有发生碰撞。(注:如果发生碰撞,growTree返回TRAPPED)。
  6. [growTree]以dstate=>motion->state、nmotion=>param,nmotion->root=>motion->root,把这个新生成的motion结点加入扩展树,树中结点数2。随即growTree返回ADVANCED,表示此次growTree在随机采样状态方面取得了进展,这个进展应该就是的加入了一个新结点motion。而且tgi.xmotion指向这个新加入的结点。
  7. 因为growTree不是返回TRAPPED,进入目标树环节。由于growTree不是返回REACHED,更新上一次growTree内新产生的采样点到rstate,即rstate值是(-0.75934, 2.03224, -0.00759)。
  8. [第二次growTree]nearest(rmotion)找到离rmotion最近的目标树中结点(1, 2, -1),记为nmotion。
  9. [growTree]算出nmotion和rmotion两结点距离是2.78395,大于扩展阀值1.884。于是以nmotion为起点,最大可扩展距离1.884为增加值,生成新采样点(-0.19061, 2.02185, -0.32842),记为dstate。
  10. [growTree]checkMotion(dstate)进行碰撞检测,结果是没有发生碰撞。
  11. [growTree]以dstate=>motion->state、nmotion=>param,nmotion->root=>motion->root,把这个新生成的motion结点加入扩展树,树中结点数2。随即growTree返回ADVANCED,表示此次growTree在随机采样状态方面取得了进展,这个进展应该就是的加入了一个新结点motion。而且tgi.xmotion指向这个新加入的结点。
  12. 因为growTree返回ADVANCED,继续在目标树调用growTree。此时rmotion依旧是(-0.75934, 2.03224, -0.00759),不是最近一次的(-0.19061, 2.02185, -0.32842)。
  13. [第三次growTree]nearest(rmotion)找到离rmotion最近的目标树中结点(-0.19061, 2.02185, -0.32842),记为nmotion。
  14. [growTree]算出nmotion和rmotion两结点距离是0.89995,<=扩展阀值1.884。于是保持dstate值是rmotion、reach=true。
  15. [growTree]checkMotion(dstate)进行碰撞检测,结果是没有发生碰撞。
  16. [growTree]以dstate=>motion->state、nmotion=>param,nmotion->root=>motion->root,把这个新生成的motion结点加入目标树,树中结点数3。随即growTree返回REACHED,表示此次growTree达到随机采样rmotion,而且tgi.xmotion指向这个rmotion。注:rmotion结点已经在起点树,但这里操作的是目标树,这里的增加是加到目标树。
  17. 由于growTree返回REACHED,退出while (gsc == ADVANCED),继续solve后续逻辑。
  18. startMotion: (-0.75934, 2.03224, -0.00759), goalMotion: (-0.75934, 2.03224, -0.00759)
  19. 为避免生成的路径中含有重复结点,要让startMotion或goalMotion当中一个往后退一代。首选是先退startMotion。两棵树中,根结点的parent都是nullptr。
  20. 根据初始树生成路径mpath1。生成方法是从startMotion开始,通过parent向上溯。由于startMotion就是初始结点(-0.56, 0.79, -0.45),mpath1长度是1。
  21. 根据目标树生成路径mpath2。生成方法是从goalMotionMotion开始,通过parent向上溯。goalMotionMotion就是起始结点(-0.75934, 2.03224, -0.00759),mpath2长度是3。
  22. 合并path1、path2,结果放在path_的states_。states_长度是4,第一个是起始点,最后一个是目标点。

[ INFO] [1648085719.153534100]: robot_arm/robot_arm: Created 5 states (2 start + 3 goal)。——总共创建了5个结点,初始树中2个,目标树中3个。

图2 RRTConnect::solve后生成的path_

图2显示了RRTConnect::solver后生成的路径,有4个点,就是图1从初始树的初始结点,一到目标树的目标结点。

PathGeometric::length()返回的不是states_长度,而是各点之间的距离和(distance)。

 

二、RRTConnect::growTree

新来了一个采样点rmotion,看如何加入随机树tree。如果肯定能通过冲突检测,怎么加入分两种情况:1)采样点位在可扩展半径内,该采样点加入随机树。2)采样点落在可扩展半径外,在nmotion和rmotion之间新算出一个采样点,把该采样点加入随机树。

@tree:要生长的树。可能是初始树,可能是目标树,可根据tgi.start判断是哪种树。

@tgi:struct TreeGrowingInfo。用于在计算过程中存储输助信息。1)growTree时,光一个GrowState返回值是不够的,它的xstate、xmotion也存储着“返回值”。2)start字段指示了参数tree是tStart_还是tGoal_。

  • base::State *xstate。如果此次growTree发生了interpolate,它存储着interpolate出的结点值。
  • Motion *xmotion。此个growTree向随机树新添加的结点。
  • bool start。true表示此次处理随机树tree是tStart_,否则处理的是tGoal_。

@rmotion:class Motion。一个Motion表示一个树中结点。此处表示一个新采样出的结点,该结点肯定没在tree中。

  • const base::State *root。该结点从此root所在结点扩展出来。以它为根,会有一条枝桠,枝桠上所有结点有着同一个root。对初始树,它就是初始结点,对目标树,它就是目标结点。在图1目标树中的三个结点,它们root都是(1, 2, -1)。
  • base::State *state。结点值。
  • Motion *parent。该结点的直系父结点。初始树中初始结点、目标树中目标结点的parent是nullptr,其它结点都是非nullptr。在图1中目标树中,(-0.75934, 2.03224, -0.00759)的parent是(-0.19061, 2.02185, -0.32842)。

返回值:enum GrowState。

  • TRAPPED。在新采样会发生碰撞,没有取得任何进展。
  • ADVANCED。在随机采样状态方面取得了进展。向随机树添清加一个结点,但添加的不是希望的rmotion。
  • REACHED。成功扩展到随机采样点rmotion。向随机树添清加rmotion结点。
ompl::geometric::RRTConnect::GrowState ompl::geometric::RRTConnect::growTree(TreeData &tree, TreeGrowingInfo &tgi,
                                                                             Motion *rmotion)
{
    /* find closest state in the tree */
    Motion *nmotion = tree->nearest(rmotion);

找出随机树中离rmotion最近结点,新指针nmotion指向树上这个结点。

    /* assume we can reach the state we go towards */
    bool reach = true;

    /* find state to add */
    base::State *dstate = rmotion->state;
    double d = si_->distance(nmotion->state, rmotion->state);

计算nmotion、rmotion之间距离。计算距离时要用到机器人模型。

    if (d > maxDistance_)
    {
        si_->getStateSpace()->interpolate(nmotion->state, rmotion->state, maxDistance_ / d, tgi.xstate);

距离大于一次可扩展的最大距离,则插入中间点。中间点是以nmotion为起点,朝向rmotion扩展出maxDistance_距离。注意,tgi.xstate存储了此个中间点的值。

        /* Check if we have moved at all. Due to some stranger state spaces (e.g., the constrained state spaces),
         * interpolate can fail and no progress is made. Without this check, the algorithm gets stuck in a loop as it
         * thinks it is making progress, when none is actually occurring. */
        if (si_->equalStates(nmotion->state, tgi.xstate))
            return TRAPPED;

si_->equalStates逻辑基本可认为就是判断nmotion->state、tgi.xstate数值上是否相等。

        dstate = tgi.xstate;
        reach = false;
    }

    bool validMotion = tgi.start ? si_->checkMotion(nmotion->state, dstate) :
                                   si_->isValid(dstate) && si_->checkMotion(dstate, nmotion->state);

checkMotion执行冲突检测。

    if (!validMotion)
        return TRAPPED;

    if (addIntermediateStates_)
    {
        // addIntermediateStates_默认false,不进这入口
    }
    else
    {
        auto *motion = new Motion(si_);
        si_->copyState(motion->state, dstate);
        motion->parent = nmotion;
        motion->root = nmotion->root;
        tree->add(motion);

        tgi.xmotion = motion;

向随机树添加一个结点。reach=true时,这个结点就是rmotion,否则是interpolate。注意,tgi.xmotion指向这个新加入的结点。

    }

    return reach ? REACHED : ADVANCED;
}

 

三、RevoluteJointModel::interpolate

growTree计算nmotion和rmotion距离,当距离大于一次可扩展的最大距离,要插入中间点。中间点是以nmotion为起点,朝向rmotion扩展出maxDistance_距离。生成中间点,说来就是要生成这State表示的m维值。针对示例,有三个单自由度关节,这个State的是个3维值。生成一个维度值方法是调用一次RevoluteJointModel::interpolate,3维意味着要调用三次。

  • @from: 起始值。对示例中的从nmotion(-0.56, 0.79, -0.45)到rmotion(-0.87008, 2.72230, 0.23816),对a关节,from是-0.56。
  • @to: 终点值。对@from示例,对a关节,to是-0.87008。
  • @t: 范围(0, 1)double系数。对@from示例,值来自公式“maxDistance_ / d”,即“1.884 / 2.93054”。
void RevoluteJointModel::interpolate(const double* from, const double* to, const double t, double* state) const
{
  if (continuous_)
  {

continuous_:该关节是否做圆周运动。true表示做圆周运动。

    ...
  }
  else
    state[0] = from[0] + (to[0] - from[0]) * t;
}

后面“ompl生成路径[2/2]:simplifySolution、interpolateSolution”,调用pg.interpolate补插path_时,也是调用RevoluteJointModel::interpolate,这些新插入的平均分布在该段。

 

四、RRTConnect::solve

ompl::base::PlannerStatus ompl::geometric::RRTConnect::solve(const base::PlannerTerminationCondition &ptc)
{
    checkValidity();
    auto *goal = dynamic_cast<base::GoalSampleableRegion *>(pdef_->getGoal().get());

    if (goal == nullptr)
    {
        OMPL_ERROR("%s: Unknown type of goal", getName().c_str());
        return base::PlannerStatus::UNRECOGNIZED_GOAL_TYPE;
    }

    while (const base::State *st = pis_.nextStart())
    {
        auto *motion = new Motion(si_);
        si_->copyState(motion->state, st);
        motion->root = motion->state;
        tStart_->add(motion);

把初始结点添加到tStart_树,这些结点parent是nullptr。对示例,只一个初始结点。

    }

    if (tStart_->size() == 0)
    {
        OMPL_ERROR("%s: Motion planning start tree could not be initialized!", getName().c_str());
        return base::PlannerStatus::INVALID_START;
    }

    if (!goal->couldSample())
    {
        OMPL_ERROR("%s: Insufficient states in sampleable goal region", getName().c_str());
        return base::PlannerStatus::INVALID_GOAL;
    }

    if (!sampler_)
        sampler_ = si_->allocStateSampler();

    OMPL_INFORM("%s: Starting planning with %d states already in datastructure", getName().c_str(),
                (int)(tStart_->size() + tGoal_->size()));

    TreeGrowingInfo tgi;
    tgi.xstate = si_->allocState();

    Motion *approxsol = nullptr;
    double approxdif = std::numeric_limits<double>::infinity();
    auto *rmotion = new Motion(si_);
    base::State *rstate = rmotion->state;

构造一个随机结点rmotion。在“while (!ptr)”,该结点将一直存在,但会有两次更新值。1)更新到一个sampleUniform随机值。2)如果在扩展初始树时添加的是interpolate结点,更新到interpolate值。不论何时,rmotion的parent、root一直是nullptr。

    bool solved = false;

    while (!ptc)
    {
        TreeData &tree = startTree_ ? tStart_ : tGoal_;
        tgi.start = startTree_;
        startTree_ = !startTree_;
        TreeData &otherTree = startTree_ ? tStart_ : tGoal_;

进入第N轮迭代。在每轮迭代,是tStart_还是tGoal_作为初始树,由bool变量startTree_决定,是交替的。换句话说,第一轮时tStart_是初始树tree,tGoal_是目标树otherTree。到第二轮,换作tGoal_是初始树tree_,tStart_是目标树otherTree。第三轮和第一轮一样,依此类推。

        if (tGoal_->size() == 0 || pis_.getSampledGoalsCount() < tGoal_->size() / 2)
        {
            const base::State *st = tGoal_->size() == 0 ? pis_.nextGoal(ptc) : pis_.nextGoal();
            if (st != nullptr)
            {
                auto *motion = new Motion(si_);
                si_->copyState(motion->state, st);
                motion->root = motion->state;
                tGoal_->add(motion);

把目标结点添加到tGoal_树,这些结点parent是nullptr。对示例,只一个目标结点。

            }

            if (tGoal_->size() == 0)
            {
                OMPL_ERROR("%s: Unable to sample any valid states for goal tree", getName().c_str());
                break;
            }
        }

        /* sample random state */
        sampler_->sampleUniform(rstate);

随机产生一个采样点,值存在rstate。由于rstate是所有点是结点rmotion,于是rmotion同时更新到这个采样值。接下就是调用growTree,以rmotion为新采样点,在对初始树进行扩展。

        GrowState gs = growTree(tree, tgi, rmotion);

        if (gs != TRAPPED)
        {
            /* remember which motion was just added */
            Motion *addedMotion = tgi.xmotion;

            /* attempt to connect trees */

            /* if reached, it means we used rstate directly, no need to copy again */
            if (gs != REACHED)
                si_->copyState(rstate, tgi.xstate);

进这个入口,意味着gs返回值或是REACHED,或是ADVANCED。不论哪种,初始树是肯定加了一个结点,而tgi.xmotion指向那个新添加初始树的结点。

gs != REACHED,意味着向初始树添加的是interpolate结点,此时tgi.xstate存储着这个interpolate结点值,copyState把这值更新到rmotion。也就是说,到此刻rmotion的结点值(state字段)肯定等于addedMotion,但root、parent可能不一样,rmotion的这两个字段总是nullptr。

            tgi.start = startTree_;

            /* if initial progress cannot be done from the otherTree, restore tgi.start */
            GrowState gsc = growTree(otherTree, tgi, rmotion);
            if (gsc == TRAPPED)
                tgi.start = !tgi.start;

            while (gsc == ADVANCED)
                gsc = growTree(otherTree, tgi, rmotion);

不断扩展目标树,直到或扩展到了rmotion,或遇到了碰撞冲突、无法再扩展。

            /* update distance between trees */
            const double newDist = tree->getDistanceFunction()(addedMotion, otherTree->nearest(addedMotion));
            if (newDist < distanceBetweenTrees_)
            {
                distanceBetweenTrees_ = newDist;
                // OMPL_INFORM("Estimated distance to go: %f", distanceBetweenTrees_);
            }

            Motion *startMotion = tgi.start ? tgi.xmotion : addedMotion;
            Motion *goalMotion = tgi.start ? addedMotion : tgi.xmotion;

tig.xmotion是最近一次添加到目标树结点,如果目标树成功扩展到rmotion(gsc == REACHED),意味着tgi.xmotion的state值等于addedMotion,但它们一个属于目标树,一个属于初始树,root、parent都不一样。针对图1,startMotion、goalMotion就是两棵树都共同存在的结点值(-0.75934, 2.03224, -0.00759),图中归为一个,实际是互不关联的两个。startMotion指向初始树这个,goalMotion指向目标树那个。

            /* if we connected the trees in a valid way (start and goal pair is valid)*/
            if (gsc == REACHED && goal->isStartGoalPairValid(startMotion->root, goalMotion->root))
            {
                // it must be the case that either the start tree or the goal tree has made some progress
                // so one of the parents is not nullptr. We go one step 'back' to avoid having a duplicate state
                // on the solution path
                if (startMotion->parent != nullptr)
                    startMotion = startMotion->parent;
                else
                    goalMotion = goalMotion->parent;

startMotion和goalMotion虽是两个结点,但值是相同的,为避免生成的路径中含有相同值的结点,要让startMotion或goalMotion当中一个往后退一代。优先是先退startMotion。两棵树中,有且只有根结点的parent都是nullptr。

                connectionPoint_ = std::make_pair(startMotion->state, goalMotion->state);

                /* construct the solution path */
                Motion *solution = startMotion;
                std::vector<Motion *> mpath1;
                while (solution != nullptr)
                {
                    mpath1.push_back(solution);
                    solution = solution->parent;
                }

根据初始树生成路径mpath1。生成方法是从startMotion开始,通过parent向上溯。

                solution = goalMotion;
                std::vector<Motion *> mpath2;
                while (solution != nullptr)
                {
                    mpath2.push_back(solution);
                    solution = solution->parent;
                }

根据目标树生成路径mpath2。生成方法是从goalMotionMotion开始,通过parent向上溯。

                auto path(std::make_shared<PathGeometric>(si_));
                path->getStates().reserve(mpath1.size() + mpath2.size());
                for (int i = mpath1.size() - 1; i >= 0; --i)
                    path->append(mpath1[i]->state);
                for (auto &i : mpath2)
                    path->append(i->state);

                pdef_->addSolutionPath(path, false, 0.0, getName());

合并path1、path2,结果放在path_的states_。states_第一个是起始点,最后一个是目标点。

                solved = true;
                break;
            }
            else
            {
                // We didn't reach the goal, but if we were extending the start
                // tree, then we can mark/improve the approximate path so far.
                if (tgi.start)
                {
                    // We were working from the startTree.
                    double dist = 0.0;
                    goal->isSatisfied(tgi.xmotion->state, &dist);
                    if (dist < approxdif)
                    {
                        approxdif = dist;
                        approxsol = tgi.xmotion;
                    }
                }
            }
        }
    }

    si_->freeState(tgi.xstate);
    si_->freeState(rstate);
    delete rmotion;

    OMPL_INFORM("%s: Created %u states (%u start + %u goal)", getName().c_str(), tStart_->size() + tGoal_->size(),
                tStart_->size(), tGoal_->size());

    if (approxsol && !solved)
    {
        ...
    }

    return solved ? base::PlannerStatus::EXACT_SOLUTION : base::PlannerStatus::TIMEOUT;
}

 

 

全部评论: 0

    写评论: