Skip to main content

Task Space Regions (TSR)

A Python library for pose-constrained manipulation planning using Task Space Regions.

TSRs encode any manipulation constraint as a bounded region in SE(3): grasping, placing, transporting, pouring, tool use, and more. A planner samples from a TSR to get valid end-effector poses satisfying the constraint.

Based on the IJRR paper "Task Space Regions: A Framework for Pose-Constrained Manipulation Planning" by Berenson, Srinivasa, and Kuffner.

Grasps

TSR Grasp Templates

Stable placements

Stable Placements — primitives

Cylinder, box, sphere, and torus in every stable pose on a table surface.

Stable Placements — non-convex meshes

L-shape, T-shape, and a mug (cylinder + handle) — place_mesh applied to arbitrary vertex clouds. Each object is shown in all stable orientations above the 5° margin threshold.

Installation

The distribution is named sstsr on PyPI; the import name is tsr.

pip install sstsr      # or: uv add sstsr
import tsr             # the import name is unchanged

For visualization support:

pip install "sstsr[viz]"     # or: uv add "sstsr[viz]"

For development:

git clone https://github.com/personalrobotics/tsr.git
cd tsr
uv sync --extra test

Quick Start

Load templates for a full manipulation task

Templates are YAML files that encode pose constraints for each step of a task. The library ships with two narratives:

from tsr import load_package_template
import numpy as np

# --- Tool use: screwdriver ---
grasp = load_package_template("grasps", "screwdriver_grasp.yaml")
drive = load_package_template("tasks",  "drive_screw.yaml")
drop  = load_package_template("places", "toolchest_drop.yaml")

screwdriver_pose = np.eye(4)
screwdriver_pose[:3, 3] = [0.4, 0.1, 0.02]

gripper_poses = grasp.sample(screwdriver_pose)

# --- Everyday manipulation: mug of water ---
pick      = load_package_template("grasps", "mug_handle_grasp.yaml")
carry     = load_package_template("tasks",  "mug_transport_upright.yaml")
pour      = load_package_template("tasks",  "mug_pour_into_sink.yaml")
place     = load_package_template("places", "mug_on_table.yaml")

These packaged YAML files are illustrative, hand-authored pose-constraint recipes. They are tested for representation validity, but they do not carry the geometric soundness guarantee of templates produced by the primitive grasp factories below. Users remain responsible for checking them against the intended object and gripper.

Generate templates from object geometry

Grasping

ParallelJawGripper generates TSR templates directly from shape parameters. Pre-configured subclasses are provided for common grippers:

Values match the class constants. finger_length is the graspable depth along the approach axis, from the TSR "palm" to the pad-contact point; the reference point for that measurement is listed explicitly (they are not interchangeable).

Class finger_length max_aperture finger_length reference (palm → pad tip)
Robotiq2F140 114 mm 128 mm palm (grasp_site, base_mount + 100 mm) → pad tip
Robotiq2F85 59 mm 85 mm palm (housing forward edge) → pad tip
FrankaHand 37 mm 80 mm palm (hand-body forward edge) → pad tip
import numpy as np
from tsr.hands import ParallelJawGripper, Robotiq2F85, Robotiq2F140, FrankaHand

# Use a pre-configured gripper for a known hardware platform
gripper = Robotiq2F140()
gripper = FrankaHand()

# Or configure manually for custom hardware (generic 55 mm-fingered gripper)
gripper = ParallelJawGripper(finger_length=0.055, max_aperture=0.140)

# Cylinder — side + top + bottom: 4*k templates (default k=3: 12 total)
templates = gripper.grasp_cylinder(
    cylinder_radius=0.040,   # 4 cm radius
    cylinder_height=0.120,   # 12 cm tall
    reference="mug",
)

# Box — all six faces, two finger orientations per face: up to 2*6*k templates
templates = gripper.grasp_box(box_x=0.08, box_y=0.06, box_z=0.18, reference="box")

# Sphere — full SO(3) approach: k templates
templates = gripper.grasp_sphere(object_radius=0.040, reference="ball")

# Torus — side (all minor angles) + span (if aperture allows)
templates = gripper.grasp_torus(
    torus_radius=0.035,   # major radius R: center to tube center
    tube_radius=0.015,    # minor radius r: tube cross-section
    reference="handle",
)

# Instantiate at a specific object pose and sample
mug_pose = np.eye(4)
mug_pose[:3, 3] = [0.5, 0.0, 0.0]   # mug at x=0.5m

grasp_poses = [t.sample(mug_pose) for t in templates]

Placing

StablePlacer generates one TSR template per stable resting pose on a flat surface:

import numpy as np
from tsr.placement import StablePlacer

placer = StablePlacer(table_x=0.60, table_y=0.40)

# Analytic primitives
templates = placer.place_cylinder(cylinder_radius=0.040, cylinder_height=0.120, subject="mug")
templates = placer.place_box(lx=0.08, ly=0.06, lz=0.18, subject="box")   # up to 3 poses
templates = placer.place_sphere(radius=0.040, subject="ball")
templates = placer.place_torus(major_radius=0.035, minor_radius=0.015, subject="ring")

# Arbitrary mesh — pass any (N, 3) vertex cloud + centre of mass
vertices = np.array([...])   # (N, 3) points
com      = np.array([cx, cy, cz])
templates = placer.place_mesh(vertices, com, subject="widget",
                              min_margin_deg=5.0)  # discard marginal poses

# Each template encodes one stable orientation; sample a table pose for it
table_pose = np.eye(4)
table_pose[2, 3] = 0.75   # table surface at z = 0.75 m

for t in templates:
    pose = t.sample(table_pose)
    print(t, "→ COM z =", pose[2, 3])

Stability is determined by the COM-projection criterion: a face is stable if the centre of mass projects inside the support polygon formed by that face's contact region. The stability margin is arctan(d_min / h_com) where d_min is the minimum distance from the COM projection to any edge of the polygon.

Work directly with TSRs

from tsr import TSR, sample_haar
import numpy as np

# A TSR is defined by three components:
#   T0_w : 4×4 transform — world frame to TSR frame
#   Tw_e : 4×4 transform — TSR frame to end-effector at Bw=0
#   Bw   : 6×2 bounds — [x, y, z, roll, pitch, yaw]

# Example: keep a mug upright — free xy/yaw, small pitch/roll
T0_w = np.eye(4)
Tw_e = np.eye(4)
Bw   = np.zeros((6, 2))
Bw[0, :] = [-2.0,  2.0]       # x: anywhere in workspace
Bw[1, :] = [-2.0,  2.0]       # y: anywhere in workspace
Bw[2, :] = [ 0.5,  1.5]       # z: transport height
Bw[3, :] = [-0.26, 0.26]      # roll:  ±15°
Bw[4, :] = [-0.26, 0.26]      # pitch: ±15°
Bw[5, :] = [-np.pi, np.pi]    # yaw: free

tsr = TSR(T0_w=T0_w, Tw_e=Tw_e, Bw=Bw)

pose     = tsr.sample()             # random SE(3) pose in the region
distance, _ = tsr.distance(pose)   # distance to nearest valid pose
is_valid = tsr.contains(pose)      # containment check

# Haar-uniform rotations over the same region, reproducible under an explicit RNG
rng  = np.random.default_rng(42)
pose = sample_haar(tsr, rng)

tsr.sample() draws roll, pitch, and yaw uniformly, which covers the region but is not uniform over rotations. sample_haar samples rotations Haar-uniformly over the same region (for example, approach directions uniform over a sphere grasp).

Save and load templates

from tsr import TSRTemplate, save_template, load_template
import numpy as np

template = TSRTemplate(
    T_ref_tsr=np.eye(4),
    Tw_e=Tw_e,
    Bw=Bw,
    task="transport",
    subject="mug",
    reference="world",
    name="Mug Transport Upright",
    description="Keep mug upright during transport, ±15° tilt tolerance",
)

save_template(template, "my_template.yaml")
template = load_template("my_template.yaml")

# Bind to an object pose at runtime
tsr = template.instantiate(np.eye(4))

End-effector frame convention

All templates in this library use a canonical end-effector frame:

z = approach direction  (toward contact surface)
y = opening / spread direction  (finger opening for grippers, tool axis for others)
x = right-hand normal   (x = y × z)

AnyGrasp / GraspNet uses x = approach — convert with:

R_convert = np.array([[0, 0, -1], [0, 1, 0], [1, 0, 0]])

TSR Chains

For coupled multi-body constraints (e.g., door opening, bimanual transport):

from tsr import TSRChain

chain = TSRChain(TSRs=[hinge_tsr, handle_tsr])
pose  = chain.sample()

Visualization

The recommended viewer is interactive and browser-based:

pip install "sstsr[viser]"        # or: uv sync --extra viser
uv run python examples/viser_cylinder_grasps.py --explore
from tsr.hands import ParallelJawGripper
from tsr.viser import explore_templates, show_templates

gripper   = ParallelJawGripper(finger_length=0.08, max_aperture=0.14)
templates = gripper.grasp_cylinder_side(0.03, 0.12)

server = show_templates(templates, cylinder=(0.03, 0.12), gripper=gripper, seed=0)
server = explore_templates(templates, cylinder=(0.03, 0.12), gripper=gripper)

Open http://localhost:8080. show_templates draws a reproducibly sampled cloud; explore_templates gives a slider per free Bw coordinate so one grasp can be scrubbed through the region. Over SSH, forward the port: ssh -L 8080:localhost:8080 user@host. Details in docs/VISER.md.

Static PNG rendering (PyVista, deprecated)

from tsr.viz import TSRVisualizer, cylinder_renderer, parallel_jaw_renderer, plasma_colors

poses  = [t.sample(mug_pose) for t in templates]
colors = plasma_colors(len(templates))

TSRVisualizer(
    title="Cylinder Grasp Templates",
    focus=(0., 0., 0.06),
).render(
    reference_renderer=cylinder_renderer(radius=0.04, height=0.12),
    subject_renderer=parallel_jaw_renderer(finger_length=0.055, half_aperture=0.07),
    poses=poses,
    colors=colors,
    out="grasp_viz.png",
)

Deprecated. The PyVista renderer above (tsr.viz) will be removed in sstsr 3.0. It keeps working throughout 2.x and the viz extra is unchanged, but new code should use the interactive viewer below. See docs/MIGRATION-VISER.md.

Documentation

  • Tutorial — TSR theory, math, and worked examples
  • Examples — Runnable scripts (uv run python examples/<script>.py)

Testing

uv run pytest tests/ -v

License

BSD-2-Clause — see LICENSE file.

Release files for sstsr 2.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sstsr 2.3.0
File Size Uploaded
sstsr-2.3.0.tar.gz 2.2 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for sstsr 2.3.0
File Interpreter ABI Platform
sstsr-2.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 2.3 MB

Release files / sstsr-2.3.0.tar.gz

Download URL sstsr-2.3.0.tar.gz
Size 2.2 MB
Tags Source
SHA-256 checksum
How to use checksums
1e140c82b461b1c6fc02a7e2432d95b290f0ed01c710a218f47a132c040e2bc9
BLAKE2b-256 checksum
How to use checksums
f57059bf13c7dadcb4d6130b590c14a42ff4592a50a97e6a5b097100fb2980d1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / sstsr-2.3.0-py3-none-any.whl

Download URL sstsr-2.3.0-py3-none-any.whl
Size 85.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
497811f014ffd526907b70dd3914a891393a572b222df95f9f5aeb2162dfa68b
BLAKE2b-256 checksum
How to use checksums
5d5f5a32d8d0b6d08de5ba3982810a8f72aca7c7a2e7d86d5e6a24bcf474a94c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

2.3.0 This release

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.1

2 release files

2.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page