Inverse Kinematics
RoboPlan provides two inverse kinematics solvers: a simple Jacobian-based solver and an optimal solver based on quadratic programming.
SimpleIK: Jacobian-Based Solver
SimpleIK is a lightweight inverse kinematics solver using the damped least squares (DLS) method, also known as the Levenberg-Marquardt algorithm.
Algorithm
At each iteration, SimpleIK computes joint velocities that minimize the Cartesian error:
Where:
\(e = \log_6(T_{\text{goal}}^{-1} T_{\text{current}})\) — 6D Cartesian error (position + orientation)
\(J\) — Frame Jacobian in
LOCALreference frame\(\lambda\) — Damping factor for regularization
The joint configuration is updated via integration:
Properties:
Simple and efficient — minimal computational overhead
Supports multiple simultaneous goal frames
Collision checking with random restarts on failure
Convergence monitoring based on separate linear and angular error thresholds
Optionally attempt to find a nearest solution to the seed until the timeout is reached
Configuration
Parameter |
Description |
Default |
|---|---|---|
|
Joint group to control |
“” |
|
Maximum iterations per attempt |
100 |
|
Maximum computation time (seconds) |
0.005 |
|
Number of random restarts on failure |
2 |
|
Integration step size |
0.25 |
|
Damping factor \(\lambda\) |
0.001 |
|
Convergence threshold (meters) |
0.001 |
|
Convergence threshold (radians) |
0.001 |
|
Enable collision checking |
true |
|
Return the first solution found. |
true |
Usage Example
import numpy as np
from roboplan.core import Scene, JointConfiguration, CartesianConfiguration
from roboplan.simple_ik import SimpleIkOptions, SimpleIk
# Setup
scene = Scene("robot", urdf_path, srdf_path, package_paths)
options = SimpleIkOptions(
group_name="arm",
step_size=0.25,
max_linear_error_norm=0.001,
max_angular_error_norm=0.001,
check_collisions=True
)
ik_solver = SimpleIk(scene, options)
# Define goal
goal = CartesianConfiguration()
goal.base_frame = "base"
goal.tip_frame = "tool0"
goal.tform = target_transform # 4x4 SE(3) matrix
# Solve
start = JointConfiguration()
start.positions = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) # initial configuration
solution = JointConfiguration()
success = ik_solver.solveIk(goal, start, solution)
OInK: Optimal Inverse Kinematics
The OInK solver uses Quadratic Programming (QP) to compute joint displacements that achieve multiple objectives while respecting constraints and safety barriers.
QP Problem Formulation
OInK solves the following QP at each control step:
Subject to:
Reformulated as:
Where:
Task Priorities and Nullspace Projection
Each task carries an integer priority (default 1 = highest).
Tasks at a lower priority level (higher priority number) are projected into the nullspace of all higher-priority tasks, so they cannot fight tasks above them.
Their contribution is structurally zero in the higher-priority directions.
For each priority level \(k\), the QP uses a projected Jacobian \(J_k N_k\), where \(N_k\) is the cumulative nullspace projector built from the row-stacked Jacobians of all priority levels \(1, \ldots, k-1\). \(N_1 = I\) (no projection at the top level), so a single-priority problem reduces to the standard weighted-sum QP.
The projector is computed via a damped pseudoinverse:
where \(J_{\text{stack}}\) is the vertical stack of all priority-level Jacobians strictly above \(k\), and the same Tikhonov regularization \(\lambda\) from the QP is reused as the damping. The damping keeps \((J J^T + \lambda I)\) SPD even at singular configurations; at well-conditioned configurations (singular values \(\gg \sqrt{\lambda}\)), the expression reduces to the standard nullspace projector \(I - J^+ J\).
Tasks at the same priority level are combined linearly through their weights (no projection between them). The decision variable remains \(\Delta q\); only the per-task Jacobian is projected.
Tasks
Tasks define optimization objectives through Jacobians and errors.
FrameTask
Tracks a target 6-DOF pose (position + orientation).
Error computation:
Error saturation (prevents large jumps that invalidate CBF linearization):
Jacobian: Frame Jacobian in LOCAL_WORLD_ALIGNED coordinates, negated so QP moves toward target.
Weight matrix:
Parameter |
Description |
Default |
|---|---|---|
|
Position error weight |
1.0 |
|
Orientation error weight |
1.0 |
|
Low-pass filter gain \(\alpha\) |
1.0 |
|
Levenberg-Marquardt damping |
0.0 |
|
Saturation limit (meters) |
∞ |
|
Saturation limit (radians) |
∞ |
|
Priority level (1 = highest; see Task Priorities) |
1 |
ConfigurationTask
Drives toward a target joint configuration (null-space regularization).
Error: Manifold-aware difference: \(e = \text{difference}(q, q_{\text{target}})\)
Jacobian: \(J = -I\) (negative identity)
Weight matrix: \(W = \text{diag}(\sqrt{w_1}, \ldots, \sqrt{w_{n_v}})\)
Also accepts task_gain, lm_damping, and priority (defaults match
FrameTaskOptions).
A common pattern is to use a ConfigurationTask at a lower priority level (e.g. priority=2) as a posture / null-space regularizer that will not interfere with a higher-priority FrameTask.
Constraints vs Barriers
Hard Constraints
Hard constraints enforce strict bounds that the QP solver cannot violate:
Properties:
Exact enforcement — the solution always satisfies the constraint
State-independent bounds — same restriction regardless of distance to limit
No Jacobian needed for joint-space constraints
QP becomes infeasible if constraints conflict
Use for: Physical actuator limits, joint position/velocity bounds
Control Barrier Functions (Barriers)
Barriers enforce forward invariance of a safe set through a differential condition:
In discrete time:
Properties:
State-dependent bounds — more freedom far from boundary, tighter near it
Smooth behavior — graceful slowdown instead of abrupt stop
Requires Jacobian computation
Always feasible (soft constraint via class-K function)
Subject to linearization error in discrete time
Use for: Cartesian position bounds, collision avoidance, workspace constraints
Comparison
Aspect |
Hard Constraint |
Barrier (CBF) |
|---|---|---|
Formulation |
\(\Delta q \leq\) constant |
\(J \cdot \Delta q \leq\) \(f(\text{distance})\) |
Near boundary |
Same restriction |
Tighter (slows down) |
Far from boundary |
Same restriction |
Looser (more freedom) |
Enforcement |
Exact |
Approximate (linearization) |
Feasibility |
Can fail |
Always feasible |
Behavior |
Abrupt at limit |
Smooth approach |
Computation |
Simple |
Requires Jacobian |
Constraint Details
VelocityLimit
Enforces maximum joint velocities as hard bounds:
PositionLimit
Restricts motion based on distance to joint limits:
The gain \(\gamma \in (0, 1]\) controls aggressiveness. As \(q \to q_{\max}\), the upper bound \(\to 0\).
Barrier Details
PositionBarrier
Keeps a frame within an axis-aligned bounding box using CBF constraints.
Barrier function (for lower bound on axis \(i\)):
Barrier Jacobian:
QP constraint (using saturating class-K function):
Where:
\(\gamma\) — barrier gain (aggressiveness)
\(m\) — safety margin (conservative buffer for linearization error)
Safe displacement regularization adds to the objective:
This encourages motion toward a safe configuration when near boundaries.
Parameter |
Description |
Default |
|---|---|---|
|
Class-K function gain \(\gamma\) |
1.0 |
|
Control timestep |
required |
|
Regularization weight \(r\) |
1.0 |
|
Conservative buffer \(m\) |
0.0 |
|
Enable/disable per-axis constraints |
all |
SelfCollisionBarrier
Keeps the closest pairs of bodies in the robot’s collision model from interpenetrating, by enforcing a minimum signed distance on each tracked pair. Distances come from the narrow-phase collision check on the scene’s collision model.
Barrier function (for the \(i\)-th closest collision pair):
where \(d_i(q)\) is the signed distance between the two geometries in pair \(i\). Pairs are re-selected at every call: at each step the \(n_{\text{pairs}}\) smallest distances across the full collision model become the active constraints, so the barrier always tracks whichever pairs are most at risk.
Barrier Jacobian (built from witness points and parent-joint Jacobians):
Where:
\(n\) — unit vector from witness point 1 to witness point 2 (world frame)
\(r_k\) — vector from joint \(k\)’s origin to its witness point (lever arm)
\(J^{(k)}_p, J^{(k)}_w\) — linear and angular parts of joint \(k\)’s
LOCAL_WORLD_ALIGNEDJacobian
If the witness points coincide (\(d_i \approx 0\)) the contact normal is undefined; that Jacobian row is zeroed so the barrier degrades gracefully instead of producing NaNs.
The same safe-displacement regularization described for PositionBarrier applies.
Parameter |
Description |
Default |
|---|---|---|
|
Number of closest pairs to constrain (must be ≤ total pairs in the model) |
required |
|
Minimum allowed distance \(d_{\min}\) (meters) |
0.02 |
|
Class-K function gain \(\gamma\) |
1.0 |
|
Control timestep |
required |
|
Regularization weight \(r\) |
1.0 |
|
Conservative buffer \(m\) |
0.0 |
Note
Per-pair narrow-phase distance dominates the per-solve cost when many pairs are tracked.
Pick the smallest n_collision_pairs that still covers the pairs you expect to be active.
The post-solve enforceBarriers() check only re-evaluates this active set, so over-sizing n_collision_pairs makes both the QP assembly and the FK validation slower.
Additionally, you should consider using robot models that have optimized collision meshes (e.g., simplified convex hulls or simple geometric primitives). If your collision meshes are too high-quality, this will dramatically increase solve time.
Linearization Error and enforceBarriers()
The CBF constraint is based on a first-order Taylor expansion:
This has \(O(\|\Delta q\|^2)\) error. Near boundaries with large commands, the linearized constraint can be satisfied while the actual barrier is violated.
enforceBarriers() provides a post-solve safety check using forward kinematics:
// After solving QP
oink.solveIk(tasks, constraints, barriers, scene, delta_q);
// Validate using FK: if h(q + delta_q) < -tolerance, set delta_q = 0
oink.enforceBarriers(barriers, scene, delta_q, tolerance);
Implementation Notes
Numerical Properties
Positive definiteness: Guaranteed by Tikhonov regularization \(\lambda I\)
Convexity: Quadratic objective + linear constraints → unique global optimum
Weight scaling: Applied as \(\sqrt{w}\) for better conditioning
Solver
OInK uses OSQP with:
Dense accumulation of \(H\) and \(c\)
Sparse conversion for solving
Warm-starting between iterations
Workspace caching for constraints
Usage Example
import numpy as np
from roboplan.core import Scene, CartesianConfiguration
from roboplan.optimal_ik import (
ConfigurationTask, ConfigurationTaskOptions,
FrameTask, FrameTaskOptions,
Oink, PositionLimit, VelocityLimit,
PositionBarrier, SelfCollisionBarrier,
)
# Scene + solver. urdf/srdf are XML strings (e.g. from xacro.process_file(...).toxml()).
scene = Scene("robot", urdf=urdf_xml, srdf=srdf_xml, package_paths=package_paths)
oink = Oink(scene, group_name="arm")
nv = len(oink.v_indices) # joint-group velocity dimension
dt = 0.01
# Primary task: track an SE(3) target with the end-effector frame.
goal = CartesianConfiguration()
goal.base_frame = "base_link"
goal.tip_frame = "tool0"
goal.tform = target_transform # 4x4 SE(3) matrix
frame_task = FrameTask(oink, scene, goal, FrameTaskOptions(
position_cost=1.0,
orientation_cost=0.1,
task_gain=1.0,
lm_damping=0.01,
max_position_error=0.1, # keeps the CBF linearization valid
))
# Lower-priority posture regularization. priority=2 projects it into the FrameTask
# nullspace, so it only uses the redundant DoF the EE task leaves free.
posture_task = ConfigurationTask(
oink,
q_nominal[oink.q_indices],
np.full(nv, 0.05),
ConfigurationTaskOptions(priority=2),
)
tasks = [frame_task, posture_task]
# Hard constraints (exact enforcement).
constraints = [
PositionLimit(oink, gain=1.0),
VelocityLimit(oink, dt, v_max=np.ones(nv)),
]
# Barriers (smooth safety).
barriers = [
PositionBarrier(
oink, scene,
frame_name="tool0",
p_min=np.array([-0.5, -0.5, 0.1]),
p_max=np.array([0.5, 0.5, 1.0]),
dt=dt,
gain=5.0,
safety_margin=0.01,
),
SelfCollisionBarrier(
oink, scene,
n_collision_pairs=4, # track the 4 closest pairs each step
dt=dt,
gain=0.01,
d_min=0.02,
),
]
# One control step.
delta_q = np.zeros(nv)
oink.solveIk(scene, tasks, constraints, barriers, delta_q, regularization=1e-6)
# When the joint group is a subset of the model, scatter the group's velocity into
# the full-model nv vector before enforceBarriers / integrate.
delta_q_full = np.zeros(model_nv) # full model velocity dimension
delta_q_full[oink.v_indices] = delta_q
# Optional FK-based safety check: zeros delta_q_full if any barrier would be violated.
oink.enforceBarriers(scene, barriers, delta_q_full)
q_next = scene.integrate(scene.getCurrentJointPositions(), delta_q_full)
scene.setJointPositions(q_next)