Metadata-Version: 2.5
Name: sstsr
Version: 2.2.0
Summary: Task Space Regions for grasp and placement planning (import as `tsr`)
Project-URL: Homepage, https://github.com/personalrobotics/tsr
Project-URL: Repository, https://github.com/personalrobotics/tsr
Project-URL: Issues, https://github.com/personalrobotics/tsr/issues
Author-email: Siddhartha Srinivasa <siddhartha.srinivasa@gmail.com>
Maintainer-email: Siddhartha Srinivasa <siddhartha.srinivasa@gmail.com>
License-Expression: BSD-2-Clause
License-File: LICENSE
Keywords: motion-planning,robotics,task-space-regions,tsr
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: numpy>=1.24.0
Requires-Dist: pyyaml>=6.0.1
Requires-Dist: scipy>=1.10.0
Provides-Extra: test
Requires-Dist: build>=1.0.0; extra == 'test'
Requires-Dist: hypothesis>=6.112; extra == 'test'
Requires-Dist: pytest-cov>=4.0.0; extra == 'test'
Requires-Dist: pytest>=7.0.0; extra == 'test'
Provides-Extra: viz
Requires-Dist: matplotlib>=3.8.0; extra == 'viz'
Requires-Dist: pillow>=10.3.0; extra == 'viz'
Requires-Dist: pyvista>=0.43.0; extra == 'viz'
Description-Content-Type: text/markdown

# 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"](https://www.ri.cmu.edu/pub_files/2011/10/dmitry_ijrr10-1.pdf) by Berenson, Srinivasa, and Kuffner.

## Gallery

### Grasps

![TSR Grasp Templates](assets/tsr_grasps.png)

### Stable placements

![Stable Placements — primitives](assets/stable_placements.png)

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

![Stable Placements — non-convex meshes](assets/mesh_placements.png)

*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`.

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

For visualization support:
```bash
pip install "sstsr[viz]"     # or: uv add "sstsr[viz]"
```

For development:
```bash
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:

```python
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 |

```python
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:

```python
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

```python
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

```python
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:
```python
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):

```python
from tsr import TSRChain

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

## Visualization

```python
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",
)
```

Requires the `viz` extra: `uv sync --extra viz`.

## Documentation

- **[Tutorial](docs/tutorial.md)** — TSR theory, math, and worked examples
- **[Examples](examples/)** — Runnable scripts (`uv run python examples/<script>.py`)

## Testing

```bash
uv run pytest tests/ -v
```

## License

BSD-2-Clause — see LICENSE file.
