目标状态器(ompl::base::Goal)

目标状态器是从Goal派生的类对象,派生类较多,较常见的使用类是ConstrainedGoalSampler。

图1 采样、读取目标值逻辑

之前关于规划文章可知,client发来的moveit_msgs::Constraints类型表示的目标要求,最终要处理成目标结点,并添加到目标树tGoal。本文描述内中过程,核心是要用到一个目标状态器ConstrainedGoalSampler。

  1. ActionServerThread线程。从client发来的任务中提取出以moveit_msgs::Constraints类型表示的目标要求,处理成ConstraintSampler类型,值存储在constraint_sampler_成员。
  2. 启动goalSamplingThread线程。开始第一轮,每轮先调用samplerFunc_去采样状态,samplerFunc_是函数指针,指向ConstrainedGoalSampler::sampleUsingConstraintSampler。该函数会调用ConstrainedGoalSampler的sample()方法,采样出一个State,存储在临时变量s。然后调用addStateIfDifferent,把s添加到集合states_。此轮结束后,线程继续下一轮的sample()、存储到s、添加到states_。
  3. 在规划线程的RRTConnect::solve(...),调用PlannerInputStates的nextGoal(),nextGoal则调用GoalSampleableRegion(ConstrainedGoalSampler基类)的sampleGoal(...)读出states_中的一个State,存储在变量tempState_。继续把tempState_返回到solve,solve用它生成一个目标结点,加入目标树tGoal_。

接下深入这三个步骤,但在说它们前,了解下Goal的众多派生类。

图2 Goal及派生类

一、Goal以及派生类

ompl::base::Goal是目标状态器基类,Goal有着多级派生类。

Goal有个枚举类型的成员type_,它指示了对象是哪个派生类。

type_描述
GoalGOAL_ANY = 1 
GoalRegionGOAL_REGION = GOAL_ANY + 2这个目标状态器可计算出指定状态(state)和目标间距离,而且该状态在目标区域内的条件是这个距离小于规定的阈值。
GoalSampleableRegion

GOAL_SAMPLEABLE_REGION = 
GOAL_REGION + 4

可在目标区域进行采样
GoalStates

GOAL_STATES = 
GOAL_SAMPLEABLE_REGION + 16

 
GoalLazySamples

GOAL_LAZY_SAMPLES = 
GOAL_STATES + 32

需预定义一个可以采样的目标区域,但采样过程可能很慢。它会在单独线程中进行采样,并且随着规划持续运行,目标数可能会增加,当然增加时会保证线程安全。
ConstrainedGoalSampler等于GoalLazySamples 

给type_赋值时循寻一条规则:N级派生类是在N-1级type_上再加一个位掩码。也就是说,按“&”位掩码去算的话,如果N-1返回true,那N级去“&”同一个值时,一定返回true。举个例子,GoalSampleableRegion的type_含有GOAL_SAMPLEABLE_REGION,那它的派生类GoalStates、GoalLazySamples、ConstrainedGoalSampler去“& GOAL_SAMPLEABLE_REGION”时,一定返回true。

随着派生代数增加,派生类会不断增多(纯)虚函数。基类Goal就一个纯虚函数:ompl::base::Goal::isSatisfied(const State *st),用来判断输入的状态st是否为目标。

class GoalSampleableRegion : public GoalRegion
{
  /** \brief Return the maximum number of samples that can be asked for before repeating */
  // 返回重复前可以请求的最大样本数
  virtual unsigned int maxSampleCount() const = 0;

  /** \brief Return true if maxSampleCount() > 0, since in this case samples can certainly be produced */
  // 如果 maxSampleCount() > 0,则返回 true,因为在这种情况下,样本肯定可以生成
  bool canSample() const
  {
    return maxSampleCount() > 0;
  }
};

派生类多出的(纯)虚函数和它增加的功能有关。GoalSampleableRegion这种目标状态器声明了它可在目标区域进行采样,于是增加了一个表示目前已有多少个采样点的maxSampleCount()。canSample()不是虚函数,只要内中存有了采样点,它就返回true。

class GoalStates : public GoalSampleableRegion
{
  void sampleGoal(State *st) const override;
  virtual void addState(const State *st);
protected:
  /** \brief The goal states. Only ones that are valid are considered by the motion planner. */
  // 目标状态。只有有效的运动规划器才会考虑这些。
  std::vector<State *> states_;
};

void ompl::base::GoalStates::sampleGoal(base::State *st) const
{
    if (states_.empty())
        throw Exception("There are no goals to sample");

    // Roll over the samplePosition_ if it points past the number of states.
    samplePosition_ = samplePosition_ % states_.size();
    // Get the next state.
    si_->copyState(st, states_[samplePosition_]);

在状态器内部,维护着一个读索引samplePosition_,如果上次读出状态数组最后一个,此次读索引回到0。为避回0读出旧状态,调用者往往出会自已维护个读索引,像PlannerInputStates中的sampledGoalsCount_。

    // Increment the counter. Do NOT roll over incase a new state is added before sampleGoal is called again.
    samplePosition_++;
}

相比GoalSampleableRegion,GoalStates更进一步明确存储采样点的据结构是std::vector,添加采样点的方法是addState。也就是说,如果你的目标器可在目标区域进行采样,而且可以以std::vector数据结构存储,为简洁,那优先使用GoalStates。

 

二、ActionServerThread线程:生成constraint_sampler_,用它创建目标状态器ConstrainedGoalSampler

看这节时要对照“ompl规划[1/5]:关节规划”中“一、设置目标状态”。

要生成的constraint_sampler_封装了目标值,类型是constraint_samplers::ConstraintSampler的一种派生类,按不同规划场景,派生类有两种。

  • 关节规划:constraint_samplers::JointConstraintSampler(关节受限的采样器)
  • 位姿规划:constraint_samplers::IKConstraintSampler(位姿受限的采样器)

重复抄下如何创建状态目标器ConstrainedGoalSampler。

ompl::base::GoalPtr ompl_interface::ModelBasedPlanningContext::constructGoal()
{
  // ******************* set up the goal representation, based on goal constraints

  std::vector<ob::GoalPtr> goals;
  for (kinematic_constraints::KinematicConstraintSetPtr& goal_constraint : goal_constraints_)
  {
    constraint_samplers::ConstraintSamplerPtr constraint_sampler;
    if (spec_.constraint_sampler_manager_)
      constraint_sampler = spec_.constraint_sampler_manager_->selectSampler(getPlanningScene(), getGroupName(),
                                                                            goal_constraint->getAllConstraints());
    if (constraint_sampler)
    {
      ob::GoalPtr goal = ob::GoalPtr(new ConstrainedGoalSampler(this, goal_constraint, constraint_sampler));

此阶段要生成目标状态器ConstrainedGoalSampler,要三个参数,前两个可说是现成的,需要即时生成的只有ConstraintSampler类型的constraint_sampler,这个cs赋值给ConstrainedGoalSampler的成员变量constraint_sampler_。ConstrainedGoalSampler内中成员见“ompl规划[1/5]:关节规划”图1。

      goals.push_back(goal);
    }
  }
  ...
}

constructGoal()如何得到constraint_sampler?——来自spec_.constraint_sampler_manager_->selectSampler(...)。

constraint_samplers::ConstraintSamplerPtr
constraint_samplers::ConstraintSamplerManager::selectSampler(const planning_scene::PlanningSceneConstPtr& scene,
                                                             const std::string& group_name,
                                                             const moveit_msgs::Constraints& constr) const
{
  for (const ConstraintSamplerAllocatorPtr& sampler : sampler_alloc_)
    if (sampler->canService(scene, group_name, constr))
      return sampler->alloc(scene, group_name, constr);

sampler_alloc_存储着预定义分配器,一般来说不会有预定义,即sampler_alloc_是空。

  // if no default sampler was used, try a default one
  return selectDefaultSampler(scene, group_name, constr);
}

没有预定义分配器时,直接调用selectDefaultSampler。这函数较复杂,这里只抄和关节规划相关的。

constraint_samplers::ConstraintSamplerPtr
constraint_samplers::ConstraintSamplerManager::selectDefaultSampler(const planning_scene::PlanningSceneConstPtr& scene,
                                                                    const std::string& group_name,
                                                                    const moveit_msgs::Constraints& constr)
{
  const moveit::core::JointModelGroup* jmg = scene->getRobotModel()->getJointModelGroup(group_name);
  if (!jmg)
    return constraint_samplers::ConstraintSamplerPtr();
  ...
  ConstraintSamplerPtr joint_sampler;  // location to put chosen joint sampler if needed
  // if there are joint constraints, we could possibly get a sampler from those
  if (!constr.joint_constraints.empty())
  {

参数constr类型是moveit_msgs::Constraints,在关节规划场景,constr中存储约束的字段中只有joint_constraints是非空。顺便说下,位姿规划时,变成position_constraints、orientation_constraints同时非空。

    ROS_DEBUG_NAMED("constraint_samplers",
                    "There are joint constraints specified. "
                    "Attempting to construct a JointConstraintSampler for group '%s'",
                    jmg->getName().c_str());

    std::map<std::string, bool> joint_coverage;
    for (const std::string& joint : jmg->getVariableNames())
      joint_coverage[joint] = false;

    // construct the constraints
    std::vector<kinematic_constraints::JointConstraint> jc;
    for (const moveit_msgs::JointConstraint& joint_constraint : constr.joint_constraints)
    {
      kinematic_constraints::JointConstraint j(scene->getRobotModel());
      if (j.configure(joint_constraint))
      {
        if (joint_coverage.find(j.getJointVariableName()) != joint_coverage.end())
        {
          joint_coverage[j.getJointVariableName()] = true;
          jc.push_back(j);
        }
      }
    }

    // check if every joint is covered (constrained) by just joint samplers
    bool full_coverage = true;
    for (const std::pair<const std::string, bool>& it : joint_coverage)
      if (!it.second)
      {
        full_coverage = false;
        break;
      }

变量joint_coverage类型std::map,first是关节易记名,second则表示了urdf这否定义了该关节,一次实时值见图3。示例中所有关节都在urdf中定义了,于是full_coverage = true。

    // if we have constrained every joint, then we just use a sampler using these constraints
    if (full_coverage)
    {
      JointConstraintSamplerPtr sampler(new JointConstraintSampler(scene, jmg->getName()));
      if (sampler->configure(jc))
      {
        ROS_DEBUG_NAMED("constraint_samplers", "Allocated a sampler satisfying joint constraints for group '%s'",
                        jmg->getName().c_str());
        return sampler;
图3 selectDefaultSampler

关节规划场景在这个入口返回。返回的sampler类型是JointConstraintSampler。图3左侧显示了输入constr,右侧显示了输出sampler。sampler.bounds_存储着转换后的目标js,说来就是把原js加上tolerance。对右侧sampler中的values_,它此时不是有有效值,接下的JointConstraintSampler::sample会给它赋上最近一次的采样值。

      }
    }
    ...
  }
  ...
}

selectDefaultSampler(...)创建哪种ConstraintSampler依赖于参数constr的实时值。向上溯源,可知constr来自goal_constraints_[n].all_constraints_,计算它则在setGoalConstraints。

bool ompl_interface::ModelBasedPlanningContext::setGoalConstraints(
    const std::vector<moveit_msgs::Constraints>& goal_constraints, const moveit_msgs::Constraints& path_constraints,
    moveit_msgs::MoveItErrorCodes* error)
{
  // ******************* check if the input is correct
  goal_constraints_.clear();
  for (const moveit_msgs::Constraints& goal_constraint : goal_constraints)
  {
    moveit_msgs::Constraints constr = kinematic_constraints::mergeConstraints(goal_constraint, path_constraints);

此处算出的constr就是传给selectDefaultSampler(...)的constr。通常参数path_constraints是空,此时值全来自于goal_constraint,它就是client传过来的req.goal_constraints。

    kinematic_constraints::KinematicConstraintSetPtr kset(
        new kinematic_constraints::KinematicConstraintSet(getRobotModel()));
    kset->add(constr, getPlanningScene()->getTransforms());
    if (!kset->empty())
      goal_constraints_.push_back(kset);
  }
  ...
}

以下小结目标值依次转换到变量及类型

  1. req.goal_constraints[n]。类型:moveit_msgs::Constraints
  2. goal_constraints_[n].all_constraints_。类型:moveit_msgs::Constraints
  3. goals[n].constraint_sampler_。类型:constraint_samplers::ConstraintSampler

附上moveit_msgs::Constraints各成员概述。

moveit_msgs::Constraints
  std::string name: 空
  std::vector<moveit_msgs::JointConstraint> joint_constraints。关节规划时不为0。长度是关节数。
  std::vector<moveit_msgs::PositionConstraint> position_constraints。空间规划时为1,其它空。
  std::vector<moveit_msgs::OrientationConstraint> orientation_constraints。空间规划时为1,其它空。
  std::vector<moveit_msgs::VisibilityConstraint> visibility_constraints。空。

 

三、goalSamplingThread线程:采样出State,添加到States_

在GoalStates基础上,GoalLazySamples增加了如何采样,方法就是创建一个goalSamplingThread的线程,在该线程内进行采样。采样过程可能很慢,并且随着规划持续运行,目标数可能会增加,当然增加时会保证线程安全。

void ompl::base::GoalLazySamples::goalSamplingThread()
{
    ...
    unsigned int prevsa = samplingAttempts_;
    if (isSampling() && samplerFunc_)
    {
        OMPL_DEBUG("Beginning sampling thread computation");
        ScopedState<> s(si_);
        while (isSampling() && samplerFunc_(this, s.get()))
        {

isSampling()=true,表示调用者希望线程继续采集。samplerFunc_是个函数指针,当目标状态器是ConstrainedGoalSampler,实际指向的是ConstrainedGoalSampler::sampleUsingConstraintSampler。

            ++samplingAttempts_;
            if (si_->satisfiesBounds(s.get()) && si_->isValid(s.get()))
            {
                OMPL_DEBUG("Adding goal state");
                addStateIfDifferent(s.get(), minDist_);
            }
            else
            {
                OMPL_DEBUG("Invalid goal candidate");
            }
        }
    }
    ...
}

3.1 ConstrainedGoalSampler::sampleUsingConstraintSampler

要是成功,一次sampleUsingConstraintSampler会采出一个State,结果放在new_goal。

bool ompl_interface::ConstrainedGoalSampler::sampleUsingConstraintSampler(const ob::GoalLazySamples* gls,
                                                                          ob::State* new_goal)
{
  ...
  for (unsigned int a = gls->samplingAttemptsCount(); a < max_attempts && gls->isSampling(); ++a)
  {
    ...
    if (constraint_sampler_)
    {

constraint_sampler_存储着私有采样器,关节规划时JointConstraintSampler,位姿规划时IKConstraintSampler。

      // makes the constraint sampler also perform a validity callback
      moveit::core::GroupStateValidityCallbackFn gsvcf =
          std::bind(&ompl_interface::ConstrainedGoalSampler::stateValidityCallback, this, new_goal,
                    std::placeholders::_1,  // pointer to state
                    std::placeholders::_2,  // const* joint model group
                    std::placeholders::_3,  // double* of joint positions
                    verbose);
      constraint_sampler_->setGroupStateValidityCallback(gsvcf);

      if (constraint_sampler_->sample(work_state_, planning_context_->getMaximumStateSamplingAttempts()))
      {

调用采样器的sample方法,sample产生一个指定范围内的随机值,同时该值更新到work_state.position_。

        work_state_.update();
        if (kinematic_constraint_set_->decide(work_state_, verbose).satisfied)
        {

调用decide方法,它判断新采出的State是否满足约束。

          if (checkStateValidity(new_goal, work_state_, verbose))
            return true;

checkStateValidity把work_state_.position_状态值更新new_goal。综合以上步骤,总的是逻辑是这样的:

  1. 私有采样器constraint_sampler_调用sample(...)采样出一个状态,放在work_state.position_。
  2. checkStateValidity把work_state.position_更新到new_goal。于是sample(...)生成采样值就是要放入目标状态器的状态。
        }
        ...
      }
    }
    ...
  }
  return false;
}

3.2 JointConstraintSampler::sample

要是成功,一次sample会采出一个State,结果放在values_成员,同时更新到state.position_。

bool JointConstraintSampler::sample(moveit::core::RobotState& state,
                                    const moveit::core::RobotState& /* reference_state */,
                                    unsigned int /* max_attempts */)
{
  ...
  // enforce the constraints for the constrained components (could be all of them)
  for (const JointInfo& bound : bounds_)
    values_[bound.index_] = random_number_generator_.uniformReal(bound.min_bound_, bound.max_bound_);
  state.setJointGroupPositions(jmg_, values_);

bounds_是什么值见图3中右侧的sampler。举个例子,bounds_[0]=(0.9999, 10001),那uniformReal将产生一个(0.9999, 10001)范围内随机数。有多少个关节,就须要调用多少次uniformReal。

生成的state放在values_成员,同时更新到state.position_。

  // we are always successful
  return true;
}

至此除了state.position_外,sampler的values_也存储着最新采样值。

 

四、规划线程:nextGoal读出State,生成目标结点放入目标树

假设规划算法是RRTConnect,那规划主函数是RRTConnect::solve(...)。

ompl::base::PlannerStatus ompl::geometric::RRTConnect::solve(const base::PlannerTerminationCondition &ptc)
{
  ...
  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);
  }
  ...
}

为得到目标结点值,solve是通过pis_.nextGoal方法得到目标结点值,pis_类型是PlannerInputStates。得到值后生成目标结点,添加到目标树tGoal_。

4.1 PlannerInputStates::nextGoal

const ompl::base::State *ompl::base::PlannerInputStates::nextGoal(const PlannerTerminationCondition &ptc)
{
    ...
    if (pdef_->getGoal() != nullptr)
    {
        const GoalSampleableRegion *goal =
            pdef_->getGoal()->hasType(GOAL_SAMPLEABLE_REGION) ? pdef_->getGoal()->as<GoalSampleableRegion>() : nullptr;

根据上面所述的type_赋值规则,type_含有GOAL_SAMPLEABLE_REGION,意味着pdef_->goal_至少是GoalSampleableRegion,知道这个就可调用getGoal()->as(...),把goal安全转换到GoalSampleableRegion类型。以上这条语句有这么个结果:只要目标状态器pdef_->goal_至少是GoalSampleableRegion或从它派生,则goal!=nullptr。

        if (goal != nullptr)
        {
            time::point start_wait;
            bool first = true;
            bool attempt = true;
            while (attempt)
            {
                attempt = false;

                if (sampledGoalsCount_ < goal->maxSampleCount() && goal->canSample())
                {

goal->maxSampleCount()返回可以请求的最大样本数。goal->canSample()逻辑是“maxSampleCount()>0”,因而if条件其实只有前半部。

                    if (tempState_ == nullptr)
                        tempState_ = si_->allocState();
                    do
                    {
                        goal->sampleGoal(tempState_);

从目标状态器读出一个目标,sampleGoal(...)是怎么读的见上面GoalStates注释。pis维护着一个读索引sampledGoalsCount_,如果它>=goal->maxSampleCount(),意味着已读过目标状态器中的所有states_。

                        sampledGoalsCount_++;
                        bool bounds = si_->satisfiesBounds(tempState_);
                        bool valid = bounds ? si_->isValid(tempState_) : false;
                        if (bounds && valid)
                        {
                            ...
                            return tempState_;
                        }

                        ...
                    } while (!ptc && sampledGoalsCount_ < goal->maxSampleCount() && goal->canSample());
                }
                ...
            }
        }
    }
    return nullptr;
}

全部评论: 0

    写评论: