Joint Limit Tables

Example script: examples/differential_wrist_joint_limit_demo.py

This guide explains Joint Limit Tables, a feature for handling dynamic joint limits where one joint’s valid range depends on another joint’s current angle.

What are Joint Limit Tables?

In standard robot models, each joint has fixed minimum and maximum angle limits. However, some mechanical designs have coupled joint constraints where moving one joint changes the valid range of another joint.

../_images/differential-wrist-min-max.jpg

Joint limit tables visualize the valid region (blue) where the dependent joint can move based on the target joint’s angle. The green dot shows the current position is within limits.

Common examples include:

  • Differential wrist mechanisms: The roll and pitch joints share tendons/gears

  • Cable-driven robots: Cable routing creates coupled constraints

  • Parallel link mechanisms: Kinematic coupling between joints

The Joint Limit Table feature allows you to define these dynamic relationships.

Basic Concept

A Joint Limit Table defines:

  • Target Joint: The joint whose angle determines the limits

  • Dependent Joint: The joint whose limits change based on the target

  • Sample Angles: Discrete angles of the target joint

  • Min/Max Angles: The dependent joint’s limits at each sample angle

Target Joint Angle:  -90°  -45°   0°   45°   90°
                      |     |     |     |     |
Dependent Min:      -10°  -30°  -50°  -30°  -10°
Dependent Max:       10°   30°   50°   30°   10°

The limits are linearly interpolated between sample points.

DifferentialWristSample Robot

scikit-robot includes a sample robot with a differential wrist mechanism:

from skrobot.models import DifferentialWristSample

# Load robot with joint limit tables enabled (default)
robot = DifferentialWristSample(use_joint_limit_table=True)

# The wrist joints have coupled constraints
wrist_y = robot.WRIST_JOINT_Y  # Pitch
wrist_r = robot.WRIST_JOINT_R  # Roll

# Check current dynamic limits
print(f"WRIST_R min: {wrist_r.min_angle}")  # Changes with WRIST_Y
print(f"WRIST_R max: {wrist_r.max_angle}")  # Changes with WRIST_Y

Defining Joint Limit Tables

Joint limit tables are defined in YAML format:

joint_limit_tables:
  # WRIST_JOINT_Y affects WRIST_JOINT_R limits
  - target_joint: WRIST_JOINT_Y
    dependent_joint: WRIST_JOINT_R
    target_min_angle: -90  # degrees
    target_max_angle: 90   # degrees
    min_angles: [-10, -20, -50, -20, -10]  # at each sample point
    max_angles: [10, 20, 50, 20, 10]       # at each sample point

  # WRIST_JOINT_R affects WRIST_JOINT_Y limits (bidirectional)
  - target_joint: WRIST_JOINT_R
    dependent_joint: WRIST_JOINT_Y
    target_min_angle: -90
    target_max_angle: 90
    min_angles: [-15, -25, -45, -25, -15]
    max_angles: [15, 25, 45, 25, 15]

Apply the tables to a robot:

import yaml
from skrobot.model import create_joint_limit_table

with open('joint_limits.yaml', 'r') as f:
    config = yaml.safe_load(f)

for table_config in config['joint_limit_tables']:
    create_joint_limit_table(
        robot,
        table_config['target_joint'],
        table_config['dependent_joint'],
        table_config['target_min_angle'],
        table_config['target_max_angle'],
        table_config['min_angles'],
        table_config['max_angles'],
    )

Using Joint Limit Tables with IK

Joint limit tables are automatically respected by the IK solver:

from skrobot.models import DifferentialWristSample

robot = DifferentialWristSample(use_joint_limit_table=True)
robot.reset_manip_pose()

# IK will respect dynamic joint limits
target_coords = robot.end_coords.copy_worldcoords()
target_coords.translate([0.05, 0, 0])

robot.inverse_kinematics(
    target_coords,
    link_list=robot.arm.link_list,
    move_target=robot.end_coords,
)

# The solution will satisfy coupled constraints
print(f"WRIST_Y: {robot.WRIST_JOINT_Y.joint_angle()}")
print(f"WRIST_R: {robot.WRIST_JOINT_R.joint_angle()}")

Batch IK with Dynamic Limits

For batch IK, dynamic limits are also supported:

import numpy as np
from skrobot.coordinates import Coordinates
from skrobot.models import DifferentialWristSample

robot = DifferentialWristSample(use_joint_limit_table=True)
robot.reset_manip_pose()

# Create multiple target poses
base_pos = robot.end_coords.worldpos()
target_coords = []
for i in range(100):
    offset = np.random.uniform(-0.05, 0.05, size=3)
    c = Coordinates(pos=base_pos + offset, rot=robot.end_coords.worldrot())
    target_coords.append(c)

# Solve batch IK (automatically handles dynamic limits)
solutions, success_flags, _ = robot.batch_inverse_kinematics(
    target_coords,
    link_list=robot.arm.link_list,
    move_target=robot.end_coords,
    stop=50,
    attempts_per_pose=3,
)

print(f"Success rate: {sum(success_flags)}/{len(success_flags)}")
# All successful solutions satisfy dynamic constraints

Visualization Demo

The demo script visualizes joint limit tables in real-time:

python examples/differential_wrist_joint_limit_demo.py

Move the joint sliders or drag the end-effector gizmo to see how the valid region changes dynamically.

Key Concepts

Dynamic min_angle and max_angle

When a joint has a limit table, its min_angle and max_angle properties become dynamic:

# Without limit table: fixed values
print(joint.min_angle)  # Always returns same value

# With limit table: depends on target joint
robot.WRIST_JOINT_Y.joint_angle(0)
print(robot.WRIST_JOINT_R.min_angle)  # e.g., -50 degrees

robot.WRIST_JOINT_Y.joint_angle(1.0)
print(robot.WRIST_JOINT_R.min_angle)  # e.g., -30 degrees

Bidirectional Constraints

In differential mechanisms, constraints are often bidirectional:

  • Moving WRIST_Y changes WRIST_R’s limits

  • Moving WRIST_R changes WRIST_Y’s limits

Both directions should be defined for proper constraint handling.

JointLimitTable Class

Access the underlying table object:

table = robot.WRIST_JOINT_R.joint_min_max_table

# Get limits at a specific target angle
min_val = table.min_angle_function(target_angle)
max_val = table.max_angle_function(target_angle)

# Access raw data
print(table.sample_angles)  # Target joint sample points
print(table.min_angles)     # Min limits at each sample
print(table.max_angles)     # Max limits at each sample

API Reference

Creating Joint Limit Tables

skrobot.model.create_joint_limit_table(robot, target_joint, dependent_joint, target_min_angle, target_max_angle, min_angles, max_angles, use_degrees=True)[source]

Create and apply a joint limit table from arrays.

This is the recommended way to create joint limit tables. It accepts numpy arrays or lists directly.

Parameters:
  • robot (RobotModel) – The robot model to apply the joint limit table to.

  • target_joint (Joint or str) – The joint whose angle determines the limits. Can be a Joint object or a joint name string.

  • dependent_joint (Joint or str) – The joint whose limits depend on the target joint. Can be a Joint object or a joint name string.

  • target_min_angle (float) – Minimum angle of the target joint.

  • target_max_angle (float) – Maximum angle of the target joint.

  • min_angles (array-like) – Array of minimum angles for the dependent joint at each sample point.

  • max_angles (array-like) – Array of maximum angles for the dependent joint at each sample point.

  • use_degrees (bool) – If True (default), angles are in degrees. If False, angles are in radians.

Returns:

The created joint limit table object.

Return type:

JointLimitTable

Raises:

ValueError – If joints are not found or array lengths don’t match.

Examples

>>> # Using degrees (default)
>>> table = create_joint_limit_table(
...     robot,
...     target_joint='RARM_JOINT5',
...     dependent_joint='RARM_JOINT6',
...     target_min_angle=-90,
...     target_max_angle=90,
...     min_angles=[-10, -15, -20, -15, -10],
...     max_angles=[10, 15, 20, 15, 10]
... )
>>> # Using radians
>>> import numpy as np
>>> table = create_joint_limit_table(
...     robot,
...     target_joint='JOINT5',
...     dependent_joint='JOINT6',
...     target_min_angle=-np.pi/2,
...     target_max_angle=np.pi/2,
...     min_angles=np.deg2rad([-10, -15, -20]),
...     max_angles=np.deg2rad([10, 15, 20]),
...     use_degrees=False
... )

JointLimitTable Class

class skrobot.model.JointLimitTable(target_joint, target_min_angle, target_max_angle, min_angles, max_angles)[source]

Dynamic joint limit table based on another joint’s angle.

This class represents a joint limit table where joint B’s min/max angles depend on joint A’s current angle. The table uses linear interpolation between the discrete angle samples.

Parameters:
  • target_joint (Joint) – The joint whose angle determines the limits (joint A).

  • target_min_angle (float) – Minimum angle of the target joint (in radians).

  • target_max_angle (float) – Maximum angle of the target joint (in radians).

  • min_angles (numpy.ndarray) – Array of minimum angles for the dependent joint at each sample point.

  • max_angles (numpy.ndarray) – Array of maximum angles for the dependent joint at each sample point.

Examples

>>> table = JointLimitTable(target_joint, -np.pi/2, np.pi/2, min_arr, max_arr)
>>> current_min = table.min_angle_function(target_joint.joint_angle())
>>> current_max = table.max_angle_function(target_joint.joint_angle())
get_data_for_differentiable()[source]

Get data for differentiable IK solvers.

Returns:

Dictionary with keys: - ‘sample_angles’: Array of sample angles for interpolation - ‘min_angles’: Array of min limits at each sample - ‘max_angles’: Array of max limits at each sample

Return type:

dict

max_angle_function(target_angle)[source]

Get maximum angle of dependent joint for given target angle.

Parameters:

target_angle (float) – Current angle of the target joint (in radians).

Returns:

Maximum allowable angle for the dependent joint.

Return type:

float

min_angle_function(target_angle)[source]

Get minimum angle of dependent joint for given target angle.

Parameters:

target_angle (float) – Current angle of the target joint (in radians).

Returns:

Minimum allowable angle for the dependent joint.

Return type:

float

Source Code

Full source: https://github.com/iory/scikit-robot/blob/main/examples/differential_wrist_joint_limit_demo.py