Skip to content

Motion planning

Motions in botrail are planned, not taught point by point — you say where, the planner finds a collision-free, time-parameterized path. That inversion is what lets a layout change re-solve instead of breaking the cell.

One-shot plans

traj = scene.plan([0.6, -0.5, 0.8, 0.0, 0.4, 0.0])     # to joint positions
traj = scene.plan_to_pose((0.4, 0.1, 0.3))              # IK first, then plan

Under the hood: RRT-Connect with a deterministic seed, random-shortcut smoothing, then time parameterization against the joint limits. The result is a Trajectory — sample it, export it, or hand it to the studio (with broadcast=True, the default, connected studios get it for preview playback).

Planning is deterministic: the same scene state and goal produce the same trajectory, every time. seed selects a different deterministic exploration — useful when the default seed finds an ugly path — not a way to make it reproducible; it already is.

On a dual-arm robot, say which arm: scene.plan_to_pose(p, quat, group="left") plans the left arm's joints only, with the right arm frozen where it stands — an obstacle to the plan, like everything else — and add_segment(..., group="left") makes a motion the left arm's. A robot with one group needs no group=; see arms of one robot.

Named motions: waypoint segments

A one-shot plan answers "can it get there". A cell needs named motions that sequences can start — built as segment lists:

scene.add_segment("to_pallet", goal=drop_q)                # joint-space segment
scene.add_segment("to_pallet", goal=place_q,
                  kind="cartesian_line")                   # straight-line TCP move
scene.add_segment("to_pallet")                             # goal=None: capture the
                                                           # current configuration
traj = scene.plan_motion("to_pallet")

Segments append to the named motion (created when missing); kind is "joint" (planned, collision-free) or "cartesian_line" (the TCP moves on a straight line, followed by IK). Motions plan rest-to-rest at segment boundaries. Manage them with motion_names, motion_segments, remove_segment, clear_motion and remove_motion — or interactively in the studio's MOTION panel, which is the same list (the × on a motion's row deletes it).

clear_motion empties a motion and leaves it listed; remove_motion takes the name as well, so nothing of it is left in the project, the generated script or the hand-over set. That makes a motion usable as a question to the planner — author it, plan it, remove it:

scene.add_segment("line?", goal=carry, kind="cartesian_line")
try:
    scene.plan_motion("line?", broadcast=False)      # is there a straight line from here?
    straight = True
except ValueError:
    straight = False
finally:
    scene.remove_motion("line?")

A sequence step that starts a removed motion keeps its authored reference, and the cell stops checking out by name — sequence `cycle`: unknown motion `main` from scene.check() — until a motion of that name is authored again.

goal=None is the teach idiom: pose the robot — with the TCP gizmo, or set_tcp_target — then capture.

Path constraints

Constraints hold along a whole segment:

scene.add_segment("carry", goal=drop_q,
                  # keep the tool's +z within 10° of world-down: a carried
                  # tray stays level
                  orientation_cone=((0, 0, 1), (0, 0, -1), 0.17))

scene.add_segment("thread", goal=exit_q,
                  # keep the TCP inside a world-aligned box: through a window
                  position_box=((0.2, -0.4, 0.3), (0.8, 0.4, 0.9)))

orientation_cone=(axis_local, axis_world, angle_rad) keeps a tool axis inside a cone; position_box=(min, max) keeps the TCP inside a box. The studio's upright toggle is the common orientation-cone case with one click.

Trajectories

traj.duration          # seconds, after time parameterization
traj.joint_names
traj.times, traj.positions, traj.velocities
traj.sample(t)         # cubic Hermite, clamped to the span
traj.segments          # the sparse planned waypoints per segment
traj.segment_ends      # where each motion segment ends on the time axis

positions is densified for time parameterization; segments is the sparse planned path (shortcut waypoints for joint segments, IK follow points for Cartesian ones) — the natural input for robot-program export, one move command per waypoint. Exports are covered in Export.

In sequences

A sequence step that runs bt.seq.motion("to_pallet") plans the motion at that step, against the world as it stands at that moment — other robots frozen where they are, carried objects riding along. That is also why planned motions cannot run while conveyor tracking is latched (their waypoints would be baked against a target that keeps moving); guarded ramp moves fill that gap.