|
| 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 | +} |
0 commit comments