Skip to content

Commit 18f9b07

Browse files
authored
202508 armplanning file cleaning (#5237)
1 parent 3e8f3e8 commit 18f9b07

File tree

10 files changed

+556
-549
lines changed

10 files changed

+556
-549
lines changed

motionplan/armplanning/api.go

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
package armplanning
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/pkg/errors"
9+
commonpb "go.viam.com/api/common/v1"
10+
11+
"go.viam.com/rdk/components/arm"
12+
"go.viam.com/rdk/logging"
13+
"go.viam.com/rdk/motionplan"
14+
"go.viam.com/rdk/pointcloud"
15+
"go.viam.com/rdk/referenceframe"
16+
"go.viam.com/rdk/spatialmath"
17+
)
18+
19+
// PlanRequest is a struct to store all the data necessary to make a call to PlanMotion.
20+
type PlanRequest struct {
21+
FrameSystem *referenceframe.FrameSystem `json:"frame_system"`
22+
23+
// The planner will hit each Goal in order. Each goal may be a configuration or FrameSystemPoses for holonomic motion, or must be a
24+
// FrameSystemPoses for non-holonomic motion. For holonomic motion, if both a configuration and FrameSystemPoses are given,
25+
// an error is thrown.
26+
// TODO: Perhaps we could do something where some components are enforced to arrive at a certain configuration, but others can have IK
27+
// run to solve for poses. Doing this while enforcing configurations may be tricky.
28+
Goals []*PlanState `json:"goals"`
29+
30+
// This must always have a configuration filled in, for geometry placement purposes.
31+
// If poses are also filled in, the configuration will be used to determine geometry collisions, but the poses will be used
32+
// in IK to generate plan start configurations. The given configuration will NOT automatically be added to the seed tree.
33+
// The use case here is that if a particularly difficult path must be planned between two poses, that can be done first to ensure
34+
// feasibility, and then other plans can be requested to connect to that returned plan's configurations.
35+
StartState *PlanState `json:"start_state"`
36+
// The data representation of the robot's environment.
37+
WorldState *referenceframe.WorldState `json:"world_state"`
38+
// Set of bounds which the robot must remain within while navigating. This is used only for kinematic bases
39+
// and not arms.
40+
BoundingRegions []*commonpb.Geometry `json:"bounding_regions"`
41+
// Additional parameters constraining the motion of the robot.
42+
Constraints *motionplan.Constraints `json:"constraints"`
43+
// Other more granular parameters for the plan used to move the robot.
44+
PlannerOptions *PlannerOptions `json:"planner_options"`
45+
}
46+
47+
// validatePlanRequest ensures PlanRequests are not malformed.
48+
func (req *PlanRequest) validatePlanRequest() error {
49+
if req == nil {
50+
return errors.New("PlanRequest cannot be nil")
51+
}
52+
if req.FrameSystem == nil {
53+
return errors.New("PlanRequest cannot have nil framesystem")
54+
}
55+
56+
if req.StartState == nil {
57+
return errors.New("PlanRequest cannot have nil StartState")
58+
}
59+
if req.StartState.configuration == nil {
60+
return errors.New("PlanRequest cannot have nil StartState configuration")
61+
}
62+
if req.PlannerOptions == nil {
63+
req.PlannerOptions = NewBasicPlannerOptions()
64+
}
65+
66+
// If we have a start configuration, check for correctness. Reuse FrameSystemPoses compute function to provide error.
67+
if len(req.StartState.configuration) > 0 {
68+
_, err := req.StartState.configuration.ComputePoses(req.FrameSystem)
69+
if err != nil {
70+
return err
71+
}
72+
}
73+
// if we have start poses, check we have valid frames
74+
for fName, pif := range req.StartState.poses {
75+
if req.FrameSystem.Frame(fName) == nil {
76+
return referenceframe.NewFrameMissingError(fName)
77+
}
78+
if req.FrameSystem.Frame(pif.Parent()) == nil {
79+
return referenceframe.NewParentFrameMissingError(fName, pif.Parent())
80+
}
81+
}
82+
83+
if len(req.Goals) == 0 {
84+
return errors.New("PlanRequest must have at least one goal")
85+
}
86+
87+
if req.PlannerOptions.MeshesAsOctrees {
88+
// convert any meshes in the worldstate to octrees
89+
if req.WorldState == nil {
90+
return errors.New("PlanRequest must have non-nil WorldState if 'meshes_as_octrees' option is enabled")
91+
}
92+
obstacles := make([]*referenceframe.GeometriesInFrame, 0, len(req.WorldState.ObstacleNames()))
93+
for _, gf := range req.WorldState.Obstacles() {
94+
geometries := gf.Geometries()
95+
pcdGeometries := make([]spatialmath.Geometry, 0, len(geometries))
96+
for _, geometry := range geometries {
97+
if mesh, ok := geometry.(*spatialmath.Mesh); ok {
98+
octree, err := pointcloud.NewFromMesh(mesh)
99+
if err != nil {
100+
return err
101+
}
102+
geometry = octree
103+
}
104+
pcdGeometries = append(pcdGeometries, geometry)
105+
}
106+
obstacles = append(obstacles, referenceframe.NewGeometriesInFrame(gf.Parent(), pcdGeometries))
107+
}
108+
newWS, err := referenceframe.NewWorldState(obstacles, req.WorldState.Transforms())
109+
if err != nil {
110+
return err
111+
}
112+
req.WorldState = newWS
113+
}
114+
115+
boundingRegions, err := spatialmath.NewGeometriesFromProto(req.BoundingRegions)
116+
if err != nil {
117+
return err
118+
}
119+
120+
// Validate the goals. Each goal with a pose must not also have a configuration specified. The parent frame of the pose must exist.
121+
for i, goalState := range req.Goals {
122+
for fName, pif := range goalState.poses {
123+
if len(goalState.configuration) > 0 {
124+
return errors.New("individual goals cannot have both configuration and poses populated")
125+
}
126+
127+
goalParentFrame := pif.Parent()
128+
if req.FrameSystem.Frame(goalParentFrame) == nil {
129+
return referenceframe.NewParentFrameMissingError(fName, goalParentFrame)
130+
}
131+
132+
if len(boundingRegions) > 0 {
133+
// Check that robot components start within bounding regions.
134+
// Bounding regions are for 2d planning, which requires a start pose
135+
if len(goalState.poses) > 0 && len(req.StartState.poses) > 0 {
136+
goalFrame := req.FrameSystem.Frame(fName)
137+
if goalFrame == nil {
138+
return referenceframe.NewFrameMissingError(fName)
139+
}
140+
buffer := req.PlannerOptions.CollisionBufferMM
141+
// check that the request frame's geometries are within or in collision with the bounding regions
142+
robotGifs, err := goalFrame.Geometries(make([]referenceframe.Input, len(goalFrame.DoF())))
143+
if err != nil {
144+
return err
145+
}
146+
if i == 0 {
147+
// Only need to check start poses once
148+
startPose, ok := req.StartState.poses[fName]
149+
if !ok {
150+
return fmt.Errorf("goal frame %s does not have a start pose", fName)
151+
}
152+
var robotGeoms []spatialmath.Geometry
153+
for _, geom := range robotGifs.Geometries() {
154+
robotGeoms = append(robotGeoms, geom.Transform(startPose.Pose()))
155+
}
156+
robotGeomBoundingRegionCheck := NewBoundingRegionConstraint(robotGeoms, boundingRegions, buffer)
157+
if robotGeomBoundingRegionCheck(&motionplan.State{}) != nil {
158+
return fmt.Errorf("frame named %s is not within the provided bounding regions", fName)
159+
}
160+
}
161+
162+
// check that the destination is within or in collision with the bounding regions
163+
destinationAsGeom := []spatialmath.Geometry{spatialmath.NewPoint(pif.Pose().Point(), "")}
164+
destinationBoundingRegionCheck := NewBoundingRegionConstraint(destinationAsGeom, boundingRegions, buffer)
165+
if destinationBoundingRegionCheck(&motionplan.State{}) != nil {
166+
return errors.New("destination was not within the provided bounding regions")
167+
}
168+
}
169+
}
170+
}
171+
}
172+
return nil
173+
}
174+
175+
// PlanMotion plans a motion from a provided plan request.
176+
func PlanMotion(ctx context.Context, logger logging.Logger, request *PlanRequest) (motionplan.Plan, error) {
177+
// Calls Replan but without a seed plan
178+
return Replan(ctx, logger, request, nil, 0)
179+
}
180+
181+
// PlanFrameMotion plans a motion to destination for a given frame with no frame system. It will create a new FS just for the plan.
182+
// WorldState is not supported in the absence of a real frame system.
183+
func PlanFrameMotion(ctx context.Context,
184+
logger logging.Logger,
185+
dst spatialmath.Pose,
186+
f referenceframe.Frame,
187+
seed []referenceframe.Input,
188+
constraints *motionplan.Constraints,
189+
planningOpts map[string]interface{},
190+
) ([][]referenceframe.Input, error) {
191+
// ephemerally create a framesystem containing just the frame for the solve
192+
fs := referenceframe.NewEmptyFrameSystem("")
193+
if err := fs.AddFrame(f, fs.World()); err != nil {
194+
return nil, err
195+
}
196+
planOpts, err := NewPlannerOptionsFromExtra(planningOpts)
197+
if err != nil {
198+
return nil, err
199+
}
200+
plan, err := PlanMotion(ctx, logger, &PlanRequest{
201+
FrameSystem: fs,
202+
Goals: []*PlanState{
203+
{poses: referenceframe.FrameSystemPoses{f.Name(): referenceframe.NewPoseInFrame(referenceframe.World, dst)}},
204+
},
205+
StartState: &PlanState{configuration: referenceframe.FrameSystemInputs{f.Name(): seed}},
206+
Constraints: constraints,
207+
PlannerOptions: planOpts,
208+
})
209+
if err != nil {
210+
return nil, err
211+
}
212+
return plan.Trajectory().GetFrameInputs(f.Name())
213+
}
214+
215+
// Replan plans a motion from a provided plan request, and then will return that plan only if its cost is better than the cost of the
216+
// passed-in plan multiplied by `replanCostFactor`.
217+
func Replan(
218+
ctx context.Context,
219+
logger logging.Logger,
220+
request *PlanRequest,
221+
currentPlan motionplan.Plan,
222+
replanCostFactor float64,
223+
) (motionplan.Plan, error) {
224+
// Make sure request is well formed and not missing vital information
225+
if err := request.validatePlanRequest(); err != nil {
226+
return nil, err
227+
}
228+
logger.CDebugf(ctx, "constraint specs for this step: %v", request.Constraints)
229+
logger.CDebugf(ctx, "motion config for this step: %v", request.PlannerOptions)
230+
231+
sfPlanner, err := newPlanManager(logger, request)
232+
if err != nil {
233+
return nil, err
234+
}
235+
236+
newPlan, err := sfPlanner.planMultiWaypoint(ctx, currentPlan)
237+
if err != nil {
238+
return nil, err
239+
}
240+
241+
if replanCostFactor > 0 && currentPlan != nil {
242+
initialPlanCost := currentPlan.Trajectory().EvaluateCost(sfPlanner.scoringFunction)
243+
finalPlanCost := newPlan.Trajectory().EvaluateCost(sfPlanner.scoringFunction)
244+
logger.CDebugf(ctx,
245+
"initialPlanCost %f adjusted with cost factor to %f, replan cost %f",
246+
initialPlanCost, initialPlanCost*replanCostFactor, finalPlanCost,
247+
)
248+
249+
if finalPlanCost > initialPlanCost*replanCostFactor {
250+
return nil, errHighReplanCost
251+
}
252+
}
253+
254+
return newPlan, nil
255+
}
256+
257+
var defaultArmPlannerOptions = &motionplan.Constraints{
258+
LinearConstraint: []motionplan.LinearConstraint{},
259+
}
260+
261+
// MoveArm is a helper function to abstract away movement for general arms.
262+
func MoveArm(ctx context.Context, logger logging.Logger, a arm.Arm, dst spatialmath.Pose) error {
263+
inputs, err := a.CurrentInputs(ctx)
264+
if err != nil {
265+
return err
266+
}
267+
268+
model, err := a.Kinematics(ctx)
269+
if err != nil {
270+
return err
271+
}
272+
_, err = model.Transform(inputs)
273+
if err != nil && strings.Contains(err.Error(), referenceframe.OOBErrString) {
274+
return errors.New("cannot move arm: " + err.Error())
275+
} else if err != nil {
276+
return err
277+
}
278+
279+
plan, err := PlanFrameMotion(ctx, logger, dst, model, inputs, defaultArmPlannerOptions, nil)
280+
if err != nil {
281+
return err
282+
}
283+
return a.MoveThroughJointPositions(ctx, plan, nil, nil)
284+
}

motionplan/armplanning/arm.go

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)