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

之前关于规划文章可知,client发来的moveit_msgs::Constraints类型表示的目标要求,最终要处理成目标结点,并添加到目标树tGoal。本文描述内中过程,核心是要用到一个目标状态器ConstrainedGoalSampler。
- ActionServerThread线程。从client发来的任务中提取出以moveit_msgs::Constraints类型表示的目标要求,处理成ConstraintSampler类型,值存储在constraint_sampler_成员。
- 启动goalSamplingThread线程。开始第一轮,每轮先调用samplerFunc_去采样状态,samplerFunc_是函数指针,指向ConstrainedGoalSampler::sampleUsingConstraintSampler。该函数会调用ConstrainedGoalSampler的sample()方法,采样出一个State,存储在临时变量s。然后调用addStateIfDifferent,把s添加到集合states_。此轮结束后,线程继续下一轮的sample()、存储到s、添加到states_。
- 在规划线程的RRTConnect::solve(...),调用PlannerInputStates的nextGoal(),nextGoal则调用GoalSampleableRegion(ConstrainedGoalSampler基类)的sampleGoal(...)读出states_中的一个State,存储在变量tempState_。继续把tempState_返回到solve,solve用它生成一个目标结点,加入目标树tGoal_。
接下深入这三个步骤,但在说它们前,了解下Goal的众多派生类。

一、Goal以及派生类
ompl::base::Goal是目标状态器基类,Goal有着多级派生类。
Goal有个枚举类型的成员type_,它指示了对象是哪个派生类。
| 类 | type_ | 描述 |
| Goal | GOAL_ANY = 1 | |
| GoalRegion | GOAL_REGION = GOAL_ANY + 2 | 这个目标状态器可计算出指定状态(state)和目标间距离,而且该状态在目标区域内的条件是这个距离小于规定的阈值。 |
| GoalSampleableRegion | GOAL_SAMPLEABLE_REGION = | 可在目标区域进行采样 |
| GoalStates | GOAL_STATES = | |
| GoalLazySamples | GOAL_LAZY_SAMPLES = | 需预定义一个可以采样的目标区域,但采样过程可能很慢。它会在单独线程中进行采样,并且随着规划持续运行,目标数可能会增加,当然增加时会保证线程安全。 |
| 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;
关节规划场景在这个入口返回。返回的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); } ... }
以下小结目标值依次转换到变量及类型
- req.goal_constraints[n]。类型:moveit_msgs::Constraints
- goal_constraints_[n].all_constraints_。类型:moveit_msgs::Constraints
- 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。综合以上步骤,总的是逻辑是这样的:
- 私有采样器constraint_sampler_调用sample(...)采样出一个状态,放在work_state.position_。
- 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;
}