Skip to content

Commit c82312f

Browse files
authored
RSDK-13274 motion planning failed to avoid obstacles and created large arm movement (viamrobotics#5689)
1 parent 5c74cc0 commit c82312f

File tree

5 files changed

+213
-32
lines changed

5 files changed

+213
-32
lines changed

motionplan/armplanning/data/sanding-collision-with-wall.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

motionplan/armplanning/real_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,62 @@ func TestOrbPlanTooManySteps(t *testing.T) {
411411
test.That(t, zeros, test.ShouldBeLessThanOrEqualTo, 0)
412412
}
413413

414+
func TestSandingWallCollision(t *testing.T) {
415+
if IsTooSmallForCache() {
416+
t.Skip()
417+
return
418+
}
419+
420+
logger := logging.NewTestLogger(t).Sublogger("mp")
421+
ctx := context.Background()
422+
423+
start := time.Now()
424+
req, err := ReadRequestFromFile("data/sanding-collision-with-wall.json")
425+
test.That(t, err, test.ShouldBeNil)
426+
427+
logger.Infof("time to ReadRequestFromFile %v", time.Since(start))
428+
plan, _, err := PlanMotion(ctx, logger, req)
429+
test.That(t, err, test.ShouldBeNil)
430+
431+
t.Run("check trajectory length", func(t *testing.T) {
432+
test.That(t, len(plan.Trajectory()), test.ShouldBeGreaterThan, 3)
433+
})
434+
435+
t.Run("check collision checks pass with smaller resolution", func(t *testing.T) {
436+
// Create plan context to validate the path
437+
pc, err := newPlanContext(ctx, logger, req, &PlanMeta{})
438+
test.That(t, err, test.ShouldBeNil)
439+
440+
psc, err := newPlanSegmentContext(ctx, pc, req.StartState.LinearConfiguration(), req.Goals[0].Poses())
441+
test.That(t, err, test.ShouldBeNil)
442+
443+
trajectory := plan.Trajectory()
444+
smallResolution := 0.001
445+
446+
for j := 0; j < len(trajectory)-1; j++ {
447+
start := trajectory[j].ToLinearInputs()
448+
end := trajectory[j+1].ToLinearInputs()
449+
450+
// Default resolution passes
451+
err := psc.checkPath(ctx, start, end, false)
452+
test.That(t, err, test.ShouldBeNil)
453+
454+
// Small resolution noticed the collision when we had large jumps
455+
_, err = psc.checker.CheckStateConstraintsAcrossSegmentFS(
456+
ctx,
457+
&motionplan.SegmentFS{
458+
StartConfiguration: start,
459+
EndConfiguration: end,
460+
FS: pc.fs,
461+
},
462+
smallResolution,
463+
true,
464+
)
465+
test.That(t, err, test.ShouldBeNil)
466+
}
467+
})
468+
}
469+
414470
func BenchmarkBigPlanRequest(b *testing.B) {
415471
if IsTooSmallForCache() {
416472
b.Skip()

motionplan/constraint_checker.go

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -291,38 +291,9 @@ func (c *ConstraintChecker) CheckStateFSConstraints(ctx context.Context, state *
291291
// configuration of a segment at a given resolution value.
292292
func InterpolateSegmentFS(ci *SegmentFS, resolution float64) ([]*referenceframe.LinearInputs, error) {
293293
// Find the frame with the most steps by calculating steps for each frame
294-
maxSteps := defaultMinStepCount
295-
for frameName, startConfig := range ci.StartConfiguration.Items() {
296-
if len(startConfig) == 0 {
297-
// No need to interpolate 0dof frames
298-
continue
299-
}
300-
endConfig := ci.EndConfiguration.Get(frameName)
301-
if endConfig == nil {
302-
return nil, fmt.Errorf("frame %s exists in start config but not in end config", frameName)
303-
}
304-
305-
// Get frame from FrameSystem
306-
frame := ci.FS.Frame(frameName)
307-
if frame == nil {
308-
return nil, fmt.Errorf("frame %s exists in start config but not in framesystem", frameName)
309-
}
310-
311-
// Calculate positions for this frame's start and end configs
312-
startPos, err := frame.Transform(startConfig)
313-
if err != nil {
314-
return nil, err
315-
}
316-
endPos, err := frame.Transform(endConfig)
317-
if err != nil {
318-
return nil, err
319-
}
320-
321-
// Calculate steps needed for this frame
322-
steps := CalculateStepCount(startPos, endPos, resolution)
323-
if steps > maxSteps {
324-
maxSteps = steps
325-
}
294+
maxSteps, err := segmentStepCount(ci, resolution)
295+
if err != nil {
296+
return nil, err
326297
}
327298

328299
// Create interpolated configurations for all frames
@@ -521,3 +492,53 @@ func collisionCheckFinish(fs *referenceframe.FrameSystem, internalGeoms, static
521492
}
522493
return cg.minDistance, nil
523494
}
495+
496+
// Computes the quantity of intermediate constraint check steps that should be performed across a segment
497+
func segmentStepCount(ci *SegmentFS, resolution float64) (int, error) {
498+
// Find the frame with the most steps by calculating steps for each frame
499+
maxSteps := defaultMinStepCount
500+
for frameName, startConfig := range ci.StartConfiguration.Items() {
501+
if len(startConfig) == 0 {
502+
// No need to interpolate 0dof frames
503+
continue
504+
}
505+
endConfig := ci.EndConfiguration.Get(frameName)
506+
if endConfig == nil {
507+
return -1, fmt.Errorf("frame %s exists in start config but not in end config", frameName)
508+
}
509+
510+
// Get frame from FrameSystem
511+
frame := ci.FS.Frame(frameName)
512+
if frame == nil {
513+
return -1, fmt.Errorf("frame %s exists in start config but not in framesystem", frameName)
514+
}
515+
516+
// Calculate positions for this frame's start and end configs
517+
startPos, err := frame.Transform(startConfig)
518+
if err != nil {
519+
return -1, err
520+
}
521+
endPos, err := frame.Transform(endConfig)
522+
if err != nil {
523+
return -1, err
524+
}
525+
526+
// Compute joint step size from the largest limit range, divided by 1000
527+
jointStepSize := jointStepSizeFromLimits(frame.DoF())
528+
529+
maxSteps = max(maxSteps, CalculateStepCount(startPos, endPos, resolution))
530+
maxSteps = max(maxSteps, calculateJointStepCount(startConfig, endConfig, jointStepSize))
531+
}
532+
return maxSteps, nil
533+
}
534+
535+
// jointStepSizeFromLimits computes the joint step size as 1/1000 of the largest limit range.
536+
func jointStepSizeFromLimits(limits []referenceframe.Limit) float64 {
537+
var maxRange float64
538+
539+
for _, limit := range limits {
540+
_, _, r := limit.GoodLimits()
541+
maxRange = max(maxRange, r)
542+
}
543+
return maxRange / 1000
544+
}

motionplan/constraint_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,97 @@ func TestCollisionConstraints(t *testing.T) {
342342
}
343343
}
344344

345+
func TestCalculateJointStepCount(t *testing.T) {
346+
t.Run("no movement", func(t *testing.T) {
347+
start := []float64{0, 0, 0}
348+
end := []float64{0, 0, 0}
349+
test.That(t, calculateJointStepCount(start, end, 0.05), test.ShouldEqual, 0)
350+
})
351+
352+
t.Run("small movement under step size", func(t *testing.T) {
353+
start := []float64{0, 0, 0}
354+
end := []float64{0.04, 0, 0} // 0.04 rad < defaultJointStepSizeRadians
355+
test.That(t, calculateJointStepCount(start, end, 0.05), test.ShouldEqual, 1)
356+
})
357+
358+
t.Run("one radian movement", func(t *testing.T) {
359+
start := []float64{0, 0, 0}
360+
end := []float64{1.0, 0, 0} // 1 rad / defaultJointStepSizeRadians = 20 steps
361+
test.That(t, calculateJointStepCount(start, end, 0.05), test.ShouldEqual, 20)
362+
})
363+
364+
t.Run("large joint movement from sanding collision bug", func(t *testing.T) {
365+
// Joint 1 from TestSandingWallCollision moves ~6.7 radians
366+
start := []float64{1.06, -3.336, -0.18, 1.90, -1.53, 4.20}
367+
end := []float64{1.06, 3.379, -1.26, -0.59, 1.53, 1.06}
368+
// Joint 1 moves 6.715 rad / 0.05 = 135 steps
369+
steps := calculateJointStepCount(start, end, 0.05)
370+
test.That(t, steps, test.ShouldEqual, 135)
371+
})
372+
}
373+
374+
// TestSegmentStepCount tests that segmentStepCount correctly emits step count from either joint or cartesian excursion
375+
func TestSegmentStepCount(t *testing.T) {
376+
model, err := referenceframe.ParseModelJSONFile(utils.ResolveFile("components/arm/fake/kinematics/ur20.json"), "")
377+
test.That(t, err, test.ShouldBeNil)
378+
jointStepSize := jointStepSizeFromLimits(model.DoF())
379+
380+
fs := referenceframe.NewEmptyFrameSystem("test")
381+
err = fs.AddFrame(model, fs.World())
382+
test.That(t, err, test.ShouldBeNil)
383+
startConfig := []referenceframe.Input{1.06, -3.336, -0.18, 1.90, -1.53, 4.20}
384+
startPos, err := model.Transform(startConfig)
385+
test.That(t, err, test.ShouldBeNil)
386+
387+
t.Run("joint steps dominate when cartesian distance is small", func(t *testing.T) {
388+
// For a ur20 this config is very close to startConfig in cartesian space but far away in joint space
389+
endConfig := []referenceframe.Input{1.06, 3.379, -1.26, -0.59, 1.53, 1.06}
390+
segment := &SegmentFS{
391+
StartConfiguration: referenceframe.FrameSystemInputs{model.Name(): startConfig}.ToLinearInputs(),
392+
EndConfiguration: referenceframe.FrameSystemInputs{model.Name(): endConfig}.ToLinearInputs(),
393+
FS: fs,
394+
}
395+
endPos, err := model.Transform(endConfig)
396+
test.That(t, err, test.ShouldBeNil)
397+
398+
cartesianSteps := CalculateStepCount(startPos, endPos, 1.0)
399+
jointSteps := calculateJointStepCount(startConfig, endConfig, jointStepSize)
400+
401+
totalSteps, err := segmentStepCount(segment, 1.0)
402+
test.That(t, err, test.ShouldBeNil)
403+
404+
// Joint steps should dominate over cartesian for this trajectory
405+
test.That(t, jointSteps, test.ShouldBeGreaterThan, cartesianSteps)
406+
407+
// segmentStepCount should return the joint step count
408+
test.That(t, totalSteps, test.ShouldEqual, jointSteps)
409+
})
410+
411+
t.Run("cartesian steps dominate when joint distance is small", func(t *testing.T) {
412+
// Joints are close to startConfig, but quite far in cartesian space
413+
endConfig := []referenceframe.Input{1.06, -3.379, -1.26, -0.59, 1.53, 1.06}
414+
segment := &SegmentFS{
415+
StartConfiguration: referenceframe.FrameSystemInputs{model.Name(): startConfig}.ToLinearInputs(),
416+
EndConfiguration: referenceframe.FrameSystemInputs{model.Name(): endConfig}.ToLinearInputs(),
417+
FS: fs,
418+
}
419+
endPos, err := model.Transform(endConfig)
420+
test.That(t, err, test.ShouldBeNil)
421+
422+
cartesianSteps := CalculateStepCount(startPos, endPos, 1.0)
423+
jointSteps := calculateJointStepCount(startConfig, endConfig, jointStepSize)
424+
425+
totalSteps, err := segmentStepCount(segment, 1.0)
426+
test.That(t, err, test.ShouldBeNil)
427+
428+
// Cartesian steps should dominate over joint for this trajectory
429+
test.That(t, cartesianSteps, test.ShouldBeGreaterThan, jointSteps)
430+
431+
// segmentStepCount should return the cartesian step count
432+
test.That(t, totalSteps, test.ShouldEqual, cartesianSteps)
433+
})
434+
}
435+
345436
func BenchmarkCollisionConstraints(b *testing.B) {
346437
// define external obstacles
347438
bc, err := spatial.NewBox(spatial.NewZeroPose(), r3.Vector{2, 2, 2}, "")

motionplan/utils.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,15 @@ func CalculateStepCount(seedPos, goalPos spatialmath.Pose, stepSize float64) int
2222
nSteps := math.Max(math.Abs(mmDist/stepSize), math.Abs(utils.RadToDeg(rDist.Theta)/stepSize))
2323
return int(nSteps) + 1
2424
}
25+
26+
func calculateJointStepCount(start, end []float64, stepSize float64) int {
27+
steps := 0
28+
for idx, s := range start {
29+
if idx >= len(end) {
30+
break
31+
}
32+
mySteps := int(math.Ceil(math.Abs(end[idx]-s) / stepSize))
33+
steps = max(mySteps, steps)
34+
}
35+
return steps
36+
}

0 commit comments

Comments
 (0)