Skip to content

Multi-purpose hands (bt.tools)

A bracket carrying several tools — a gripper mount, pins, forks — each with its own tip frame, built as a joint-less robot model to weld on with Robot.attach_tool; and the process-tool envelopes that go on it, a rotary bur and a screwdriver with its own stroke. See Machine tending and Assembly and fastening.

bracket = bt.tools.multi_tool("hand", [bt.tools.Mount("gripper"), bt.tools.Pin("pusher"), bt.tools.Fork("fork")])
hand = bracket.attach_tool(coupling, flange="hand_gripper").attach_tool(gripper)
robot = arm.attach_tool(hand)

tools

Multi-purpose hands: one bracket, several tools, each with its own tip.

The end-effector in a machine-tending photograph is rarely one thing. A plate on the flange carries a gripper for the workpiece, a pin for the buttons and a fork for the door handle, and the robot switches between them by turning its wrist so the right one faces the job — no tool changer, no second robot. What the cell needs from such a hand is a tip frame per tool to aim at, and the bracket's own geometry in the collision check.

multi_tool builds that bracket as a robot model (a joint-less URDF welded on with Robot.attach_tool): a round plate on the flange, and on it any number of pins, forks and gripper mounts, placed in the plate's frame (+Z out of the flange). Each tool is a link with geometry and a <name>_<tool>_tip frame whose +Z points along the tool — the axis a press, a hook or a grasp approaches on — so the same idiom serves every tool: scene.set_tcp_target(pos, quat, link=tip).

bracket = bt.tools.multi_tool("hand", [bt.tools.Mount("gripper"),
                                       bt.tools.Pin("pusher"),
                                       bt.tools.Fork("fork")])
hand = bracket.attach_tool(coupling, flange="hand_gripper").attach_tool(gripper)
robot = arm.attach_tool(hand)          # tcp = the gripper's; the pin
                                       # and fork tips keep their names

The composite's tcp_link stays the gripper's, so motions and the studio gizmo behave as for any gripper; a pose taught for the pin or the fork just names its tip. IK asked for a tip moves only the arm — the gripper's fingers are off that chain and keep their value.

rotary_bur and screwdriver are process-tool envelopes to weld on the same way (a spindle's collet, a second Mount of the bracket): a tip frame whose +Z points back along the tool axis, and one link — the cutter, the bit — that is allowed to touch the work.

ALUMINIUM module-attribute

ALUMINIUM = (0.55, 0.56, 0.58)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

BLACK_STEEL module-attribute

BLACK_STEEL = (0.06, 0.06, 0.07)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

Fork

A door hook: two prongs gap apart reaching reach from a crossbar at at along direction, lying across across. The tip frame is the seat between the prongs, seat from the crossbar, +Z along the prongs — a handle bar is taken by driving the seat onto it along that axis, and the door goes wherever the fork then goes.

across class-attribute

across = (0.0, 1.0, 0.0)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

at class-attribute

at = (-0.04, 0.0, 0.022)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

color class-attribute

color = (0.06, 0.06, 0.07)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

direction class-attribute

direction = (-1.0, 0.0, 0.0)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

gap class-attribute

gap = 0.03

Convert a string or number to a floating-point number, if possible.

name class-attribute

name = 'fork'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

prong class-attribute

prong = 0.01

Convert a string or number to a floating-point number, if possible.

reach class-attribute

reach = 0.08

Convert a string or number to a floating-point number, if possible.

seat class-attribute

seat = 0.05

Convert a string or number to a floating-point number, if possible.

Mount

Where a gripper (or any catalog tool) bolts on: a link whose +Z is the tool's mounting axis. at is the mounting face's centre in the plate frame, direction its outward normal, spin turns the tool about that normal (the finger pads' axis, on a parallel gripper).

at class-attribute

at = (0.0, 0.0, 0.012)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

direction class-attribute

direction = (0.0, 0.0, 1.0)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

name class-attribute

name = 'gripper'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

spin class-attribute

spin = 0.0

Convert a string or number to a floating-point number, if possible.

Pin

A pusher: a round pin of length and diameter from at along direction. Its tip frame sits at the free end, +Z along the pin — a button is pressed by putting the tip the button's travel into the cap along that axis.

at class-attribute

at = (0.04, 0.0, 0.022)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

color class-attribute

color = (0.06, 0.06, 0.07)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

diameter class-attribute

diameter = 0.012

Convert a string or number to a floating-point number, if possible.

direction class-attribute

direction = (1.0, 0.0, 0.0)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

length class-attribute

length = 0.06

Convert a string or number to a floating-point number, if possible.

name class-attribute

name = 'pusher'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

Robot

A parsed robot model (kinematic tree + geometry). Immutable.

allowed_collisions property

allowed_collisions

The link pairs declared to be allowed to touch, by name: the package's own and those added with allow_collisions.

dof property

dof
flange_link

Declared tool-mounting face (catalog frames.flange_frame; after attach_tool, the mounted tool's onward flange if it has one). attach_tool uses it when flange is omitted.

grasp_frames property

grasp_frames

Declared grasp-surface frames (catalog frames.grasp_frames): the fingertips of a hand, the pads of a gripper — where the product says it holds things. Empty when the source declares none; after attach_tool the mounted tool's ride along.

groups property

groups

The planning groups — the arms — by name: what every group= argument addresses. A single-arm robot has one ("arm", every joint, tipped at tcp_link); a dual-arm robot has one per arm. Declared by the catalog manifest or define_group, else read off the kinematic tree: each moving branch of a body is a group, tipped at the branch's own tool mount rather than at a fingertip.

joint_limits property

joint_limits

Per actuated joint: (lower, upper) or None for continuous joints.

joint_names property

joint_names

Actuated joint names in q-vector order.

link_names

mimic_joints property

mimic_joints

Joints that follow another joint (URDF <mimic>, USD PhysxMimicJointAPI) instead of carrying a DOF of their own: {joint: (source joint, multiplier, offset)}. Their value is multiplier * <source position> + offset, and they never appear in joint_names or in a joint position vector.

mount_link

Declared mounting face when this model is a tool (catalog frames.mount_frame). attach_tool uses it when mount is omitted, falling back to the tool's root link.

name property

name
tcp_link

Default end-effector link name: the TCP declared by a tool attachment or catalog manifest when present, otherwise the deepest leaf in the kinematic tree. A robot declared to have several arms has no single TCP — ask group(name).tip.

allow_collisions method descriptor

allow_collisions(pairs)

Declares link pairs that may touch — an SRDF's disable_collisions — and returns the robot with them added; the input is not modified. A scene already allows adjacent links and pairs that touch in nearly every pose on its own; this is for what only the machine's maker can say: the bulky torso capsules of a whole-body machine overlap in most poses, and that is the design. A catalog package carries its own (self_collision.allowed_pairs).

robot = robot.allow_collisions([("link_torso_2", "link_torso_4")])

attach_tool method descriptor

attach_tool(
    tool,
    flange=None,
    mount=None,
    offset_position=None,
    offset_quaternion=None,
    tcp=None,
    prefix=None,
    group=None,
)

Welds a tool (end-effector) onto this robot's flange and returns the composite robot; neither input is modified. The DOF vector becomes this robot's joints followed by the tool's, mimic joints included.

flange defaults to the robot's declared flange_link and mount to the tool's declared mount_link (catalog manifests declare both), falling back to the tool's root — so catalog parts attach with no arguments, and a coupling's outward face becomes the composite's flange for the next attach_tool in the stack. offset places the mount relative to the flange (e.g. a coupling's thickness); the mount must resolve to the tool's root link. tcp names a tool link to become the composite's tcp_link — otherwise a TCP declared on the tool carries over, falling back to the deepest-leaf heuristic. When both models share a link/joint name, pass prefix to namespace the tool's names.

robot = ur5e.attach_tool(coupling).attach_tool(gripper)  # catalog parts
robot = ur5e.attach_tool(
    gripper, flange="flange", mount="robotiq_arg2f_base_link",
    offset_position=(0, 0, 0.0139), tcp="tcp",
)
On a dual-arm robot, group names the arm the tool goes on: its declared flange is the default flange, and its TCP becomes the tool's. Without it, a flange on one of the arms says which.

define_group method descriptor

define_group(name, tip, joints=None, flange=None)

Declares (or redeclares, by name) a planning group and returns the robot carrying it; this robot is not modified. tip is the link a motion of the group places — an arm's TCP. joints names the actuated joints it drives; omitted, they are the joints on tip's chain minus those of any group tipped above it (a waist declared as a group of its own stays out of the arm). flange names the arm's tool-mounting face, which attach_tool(..., group=) uses.

The first declaration replaces the derived groups outright: from then on the declaration is the truth, and an arm not declared is not a group.

robot = robot.define_group("left", tip="left_ee_link",
                           joints=[f"left_joint{i}" for i in range(1, 8)])

dual_arm staticmethod

dual_arm(
    left,
    right,
    left_position=None,
    left_quaternion=None,
    right_position=None,
    right_quaternion=None,
    body=None,
    left_mount=None,
    right_mount=None,
)

Two arms on one body, as a single robot with the groups left and right (link and joint names prefixed left_ / right_): the dual-arm a single controller drives. body is the torso the arms bolt to — omitted, a bare frame named after the left arm — and left_mount / right_mount name the body links the arm roots go on (the body's root by default) at the given poses. Tools then attach per arm: attach_tool(gripper, group="left").

ur = bt.Robot.from_catalog("ur5e")
pair = bt.Robot.dual_arm(ur, ur, left_position=(0, 0.3, 0.8), left_quaternion=QL,
                         right_position=(0, -0.3, 0.8), right_quaternion=QR)
pair.groups   # ['left', 'right']

from_catalog staticmethod

from_catalog(id, revision=None, format=None)

Loads a robot or tool from the botrail model catalog (the Hugging Face dataset botrail/botrail-catalog) by id — exact (robotiq/2f/2f-85/r1) or any unambiguous shorthand (2f-85, robotiq/2f-85). Needs the optional dependency huggingface_hub (pip install botrail[catalog]).

revision pins a dataset commit SHA; without it the newest catalog is fetched and the resolved SHA is recorded in the robot's source, so saved projects and generated scripts replay bit-identically. Downloads land in the standard Hugging Face cache. format forces "urdf" or "usd"; by default the URDF is preferred. A TCP the package manifest declares (frames.tcp_default) becomes tcp_link. Packages distributed as recipe_only/metadata_only raise with a pointer to building them locally.

from_package staticmethod

from_package(path, format=None, *, catalog_root=None)

Loads a locally built catalog package directory containing manifest.yaml. Preserves its product ID, frames, BOM and mounting declarations. The revision is local-sha256:, not a Hub commit. Needs PyYAML (included in botrail[catalog]); no network request is made. Save a project to bundle its geometry for portable, offline replay.

from_urdf staticmethod

from_urdf(path)

Loads a robot from a URDF file. Mesh paths are resolved relative to the file; package:// URIs are resolved heuristically.

from_usd staticmethod

from_usd(path, articulation_root=None, search_paths=None)

Imports a robot from a USD articulation (UsdPhysics joints and rigid bodies, e.g. Isaac Sim assets). Link/joint names are the prim paths; revolute limits are converted from degrees, distances from the stage's metersPerUnit, and Y-up stages are re-modeled as Z-up. articulation_root defaults to the first prim carrying PhysicsArticulationRootAPI; search_paths resolve external (omniverse://) references against local directories.

from_xacro staticmethod

from_xacro(path)

Expands a Xacro file (no ROS required) and loads the resulting URDF.

group method descriptor

group(name)

One planning group: its joints, tip (TCP) link, flange and base.

ik method descriptor

ik(
    position,
    quaternion=None,
    link=None,
    seed=None,
    max_iters=100,
    restarts=None,
    group=None,
    centering=None,
)

Solves inverse kinematics. With quaternion=None only the position is matched. link defaults to the TCP link, seed to the neutral configuration. When the seeded solve does not converge, up to restarts further attempts run from deterministically generated seeds (limits midpoint first, then fixed-seed uniform samples within the limits) — the same call always returns the same answer. Pass restarts=0 to solve strictly from the given seed. Always returns the best configuration found; check result.converged. group names the arm to solve with (its tip is the default link); the joints of any other arm stay at the seed. centering is how strongly a redundant arm's free joints are drawn toward mid-range once the pose is met (default 0.1): 0 leaves them where the seed had them, so the answer lands next to the seed — what teaching a pose from a near one, or a step along a path, wants.

joint_values method descriptor

joint_values(positions)

Value of every joint at positions, keyed by joint name: the DOF value for actuated joints, the mimic relation for driven ones, and 0 for fixed joints.

mount method descriptor

mount(
    part,
    at,
    offset_position=None,
    offset_quaternion=None,
    prefix=None,
    group=None,
)

Bolts part — a manipulator — onto this robot's link at as an arm of its own, and returns the composite; neither input is modified. The part's root goes on at at the offset; prefix namespaces its link and joint names (required when the names collide — two of the same arm). The arm becomes a planning group named group (the prefix without its trailing _ by default) with the part's TCP and flange, so tools then attach per arm: attach_tool(gripper, group="left"). See dual_arm for the two-arm shorthand.

multi_tool

multi_tool(
    name: str,
    tools: Sequence[Union],
    *,
    plate: tuple[float, float] = (0.08, 0.012),
    color: tuple = (0.55, 0.56, 0.58),
) -> Robot

The bracket as a robot model — multi_tool_urdf loaded. See there.

multi_tool_urdf

multi_tool_urdf(
    name: str,
    tools: Sequence[Union],
    *,
    plate: tuple[float, float] = (0.08, 0.012),
    color: tuple = (0.55, 0.56, 0.58),
) -> str

The bracket as URDF text — what multi_tool loads, and what a catalog asset of the hand is written from, so the made part and the catalogued one are the same geometry.

A round plate plate = (diameter, thickness) whose root link <name>_plate sits on the robot's flange (+Z outward), with the tools on it. Every tool becomes links under <name>_:

  • a Mount — the link <name>_<mount> at its face, +Z along its normal: the flange= to attach_tool the gripper on;
  • a Pin — the pin's body <name>_<pin> and the frame <name>_<pin>_tip at its end, +Z along the pin;
  • a Fork — the crossbar and prongs under <name>_<fork> and the frame <name>_<fork>_tip at the seat between the prongs, +Z along them.

Returns a joint-less Robot to weld on with attach_tool; attach_tool a gripper onto a mount first and the composite's TCP is the gripper's. A tool drawn through the plate's own axis is not refused — a hand is its author's business — but a Mount whose face lies inside the plate would bolt a gripper into the bracket, and that is.

rotary_bur

rotary_bur(
    *,
    diameter: float,
    cutting_length: float,
    shank_diameter: float,
    exposed_shank: float,
) -> Robot

A cylindrical bur envelope, dimensions in metres.

mount is the collet nose, +Z outward. tcp is the distal tip, +Z toward the spindle, matching the toolpath axis convention. Only the separate cutter link should be allowed to contact stock. The shank inside the collet and flute geometry are omitted; this is not a model of cutting forces or proof of sufficient insertion depth.

screwdriver

screwdriver(
    *,
    body_length: float = 0.2,
    body_diameter: float = 0.086,
    nose_length: float = 0.05,
    nose_diameter: float = 0.03,
    bit_length: float = 0.06,
    bit_diameter: float = 0.008,
    stroke: Optional[float] = 0.055,
    color: tuple = (0.06, 0.06, 0.07),
) -> Robot

The screwdriver envelope as a robot model — screwdriver_urdf loaded. See there.

screwdriver_urdf

screwdriver_urdf(
    *,
    body_length: float = 0.2,
    body_diameter: float = 0.086,
    nose_length: float = 0.05,
    nose_diameter: float = 0.03,
    bit_length: float = 0.06,
    bit_diameter: float = 0.008,
    stroke: Optional[float] = 0.055,
    color: tuple = (0.06, 0.06, 0.07),
) -> str

A robotic screwdriver as an envelope, dimensions in metres: the motor body (a cylinder body_diameter wide and body_length long), a nose in front of it and the bit sticking out of the nose.

mount is the root, the face that bolts to the hand, +Z into the tool along its axis. tip is the bit's free end, turned half a turn about X so its +Z points from the tip toward the body — the toolpath axis convention rotary_bur follows, so a screw is driven by aiming the tip's +Z along the hole's outward normal.

With stroke the bit rides a prismatic joint shank (0 to stroke) along the axis: the tool's own feed — a cobot screwdriver with an embedded stroke advances the screw itself while the arm stands still, which is how bt.assembly.fasten runs a screw down (a ramp of that joint, timed from the thread). stroke=None fixes the bit for a tool the arm has to push.

Only the bit link should be allowed to touch a screw head (scene.allow_link_obstacle_contact); the body and the nose stay collision-checked, and the screw a bit carries is attached to it. Torque, speed and the fastening result are not geometry: they are the driver program bt.assembly.driver writes. Returns URDF text — what screwdriver loads, and what a catalog asset of the tool is written from, so the made envelope and the catalogued one are the same geometry.

tip

tip(name: str, tool: str) -> str

The tip frame's link name for tool on the hand name.