:github_url: https://github.com/kevinzakka/mink/tree/main/docs/tutorial/tasks_and_limits.rst
.. _tasks-and-limits:
================
Tasks and Limits
================
The :doc:`quickstart` drove a robot arm to a target pose. That works because it
makes hidden assumptions: the target is reachable, joint limits are never
approached, and the arm has enough freedom to satisfy all constraints.
In practice, these assumptions fail. When they do, naive IK will drive joints
into hard limits, produce unstable motion, or sacrifice important objectives to
satisfy arbitrary ones. Tasks and limits are mink's way of making these
trade-offs explicit and controllable. This page builds on the quickstart,
showing what breaks and how to fix it. We'll stick with the same Panda arm
throughout.
What Is a Task?
===============
In mink, a *task* expresses something the robot should try to do, not something
it must do.
Examples:
- Move an end-effector to a pose (:class:`~mink.FrameTask`)
- Keep joints near a nominal configuration (:class:`~mink.PostureTask`)
- Regulate the center of mass (:class:`~mink.ComTask`)
Tasks are *soft*: they can be violated when necessary. When multiple tasks
conflict, mink resolves them by minimizing a weighted sum of their errors.
Tasks vs. Limits
================
Tasks describe *preferences*. Limits describe *hard boundaries*.
- Tasks can be violated
- Limits cannot
Examples of limits:
- Joint position bounds (:class:`~mink.ConfigurationLimit`)
- Velocity limits (:class:`~mink.VelocityLimit`)
- Collision avoidance (:class:`~mink.CollisionAvoidanceLimit`)
Under the hood, tasks become quadratic objectives in a QP, while limits become
inequality constraints. The solver finds joint velocities that best satisfy the
tasks while strictly respecting all limits.
.. note::
:class:`~mink.ConfigurationLimit` is applied automatically when you don't
pass a custom ``limits`` list. All other limits must be added explicitly.
A Single Task in Isolation
==========================
Here's where we left off — a :class:`~mink.FrameTask` driving the end-effector
to a target:
.. code:: python
import mujoco
from mink import Configuration, FrameTask, solve_ik
model = mujoco.MjModel.from_xml_path("franka_emika_panda/mjx_scene.xml")
configuration = Configuration(model)
configuration.update_from_keyframe("home")
task = FrameTask(
frame_name="attachment_site",
frame_type="site",
position_cost=1.0,
orientation_cost=1.0,
)
task.set_target(target_pose)
dt = 0.01
for _ in range(steps):
vel = solve_ik(configuration, [task], dt, "daqp")
configuration.integrate_inplace(vel, dt)
This works when the target is comfortably within the robot's workspace. But a
single task can fail in two opposite ways.
When a Single Task Breaks
=========================
Singularities
-------------
Consider tracking a target that moves outward, requiring the arm to extend
toward its reach limit:
.. code:: python
# Move outward toward near-full extension.
start_pos = np.array([0.5, 0.0, 0.4])
end_pos = np.array([0.8, 0.0, 0.4])
As the target moves away from the base, the arm must straighten to reach. Near
full extension, the elbow approaches a **singularity**: joint velocities spike
to extreme values, the motion becomes violently unstable, and the simulation
can explode entirely.
.. raw:: html
*Without regularization: the arm becomes unstable near full extension.*
At a singularity, the Jacobian becomes ill-conditioned: even tiny task-space errors
require enormous joint velocities. The problem has no reasonable solution.
Nullspace Drift
---------------
The opposite problem occurs when the robot has *more* degrees of freedom than
the task requires. With position-only tracking (``orientation_cost=0.0``), a
7-DOF arm has four extra degrees of freedom. The solver can move the elbow,
rotate the wrist, or contort the arm in any way that doesn't affect the
end-effector position.
This is **nullspace drift** — motion in degrees of freedom that don't affect
the task. Without guidance, you might see:
- The elbow drifting to awkward angles
- Unnecessary joint motion between frames
- Different runs producing different poses
.. raw:: html
*Position-only tracking: the elbow drifts unpredictably.*
Adding Regularization
=====================
mink provides three ways to regularize the IK problem:
**Numerical damping** (always available):
- ``solve_ik(..., damping=...)`` adds a small isotropic term to the Hessian,
improving conditioning for all DOFs including floating base.
- Individual tasks can set ``lm_damping`` to add error-dependent Levenberg-
Marquardt damping, which behaves more conservatively when targets are
infeasible.
**Behavioral regularization** (recommended in practice):
- :class:`~mink.DampingTask`: Minimizes joint velocities in the actuated
subspace (does not affect floating base). Because the QP solves for Δq,
this regularizes per-step motion; for fixed dt, it corresponds to velocity
damping.
- :class:`~mink.PostureTask`: Pulls toward a reference configuration.
The behavioral tasks resolve redundancy and stabilize near singularities by
trading tracking error for smaller or more predictable motion. Either can help
with both problems, but they bias the solution differently:
.. list-table::
:header-rows: 1
:widths: 20 40 40
* - Task
- Behavior
- Best for
* - DampingTask
- Minimizes per-step motion
- Numerical stability, when no preferred pose exists
* - PostureTask
- Pulls toward reference configuration
- Redundancy resolution, manipulation with a "home" pose
We'll show DampingTask for singularities and PostureTask for nullspace below,
but either can work for both.
Fixing Singularities
--------------------
:class:`~mink.DampingTask` penalizes large values of Δq:
.. code:: python
from mink import DampingTask
task = FrameTask(
frame_name="attachment_site",
frame_type="site",
position_cost=1.0,
orientation_cost=1.0,
)
# Regularization: prefer smaller joint velocities.
damping_task = DampingTask(model, cost=0.5)
for _ in range(steps):
vel = solve_ik(configuration, [task, damping_task], dt, "daqp")
configuration.integrate_inplace(vel, dt)
Near a singularity, the primary task demands huge joint velocities to make
progress. The damping task pushes back, forcing the solver to accept some
tracking error rather than produce extreme motion:
.. raw:: html
*With regularization: the arm accepts tracking error to stay stable.*
.. note::
Regularization does not keep the arm away from singular configurations —
it keeps the commanded velocities bounded by trading off tracking accuracy.
Fixing Nullspace Drift
----------------------
:class:`~mink.PostureTask` biases toward a reference configuration:
.. code:: python
from mink import PostureTask
posture_task = PostureTask(model, cost=0.1)
posture_task.set_target_from_configuration(configuration) # Use current as reference.
Now the nullspace is filled with a preference: "if you have freedom, use it to
stay close to home."
.. raw:: html
*With posture task: the arm stays close to its reference configuration.*
Adding Limits
=============
Regularization keeps the solver well-conditioned, but nothing prevents the
robot from violating physical constraints. Without explicit limits, joints can
exceed their mechanical range, velocities can spike beyond what motors can
deliver, and the robot can collide with itself or the environment.
Limits make these boundaries explicit.
Configuration Limits
--------------------
:class:`~mink.ConfigurationLimit` keeps joints within their mechanical range.
When you pass a custom ``limits`` list, you must add it explicitly:
.. code:: python
from mink import ConfigurationLimit
limits = [ConfigurationLimit(model)]
for _ in range(steps):
vel = solve_ik(configuration, [task], dt, "daqp", limits=limits)
configuration.integrate_inplace(vel, dt)
Velocity Limits
---------------
IK computes joint velocities, which you integrate into position commands. Because IK
outputs joint velocities that are integrated over dt, velocity limits implicitly bound
per-step position changes. If those positions change too quickly between timesteps,
the low-level controller will demand high velocities or torques to keep up —
leading to jerky motion, excessive wear, or safety faults.
:class:`~mink.VelocityLimit` caps the rate of change:
.. code:: python
from mink import VelocityLimit
velocity_limits = {
"joint1": 2.0, # rad/s
"joint2": 2.0,
# ... etc
}
velocity_limit = VelocityLimit(model, velocity_limits)
limits = [ConfigurationLimit(model), velocity_limit]
The tradeoff is tracking accuracy: if the target moves faster than the velocity
limits allow, the arm will lag behind. In practice, you tune the limits to
match your hardware's actual capabilities.
Collision Avoidance
-------------------
:class:`~mink.CollisionAvoidanceLimit` prevents specified geometries from
colliding. You define pairs of geom groups that should stay apart:
.. code:: python
from mink import CollisionAvoidanceLimit
collision_limit = CollisionAvoidanceLimit(
model,
geom_pairs=[(["hand"], ["link4", "link5"])],
minimum_distance_from_collisions=0.05,
)
limits = [ConfigurationLimit(model), collision_limit]
.. warning::
Collision avoidance constraints tighten as distances shrink. When geoms are
close to the minimum distance, this limit can dominate the solution and
prevent other motion. Start with conservative (smaller) minimum distances.
Without collision avoidance, nullspace drift can cause self-collision. With it,
the solver keeps the hand away from the arm:
.. raw:: html
*Collision avoidance: the hand stays clear of the upper arm.*
Practical Guidelines
====================
When to Use What
----------------
**Always include:**
- :class:`~mink.ConfigurationLimit` — prevents violating joint limits
- Some form of regularization (:class:`~mink.DampingTask` or
:class:`~mink.PostureTask`) — avoids singularity and nullspace issues
**Add when needed:**
- :class:`~mink.VelocityLimit` — when you need smooth, rate-limited motion
- :class:`~mink.CollisionAvoidanceLimit` — when self-collision or environment
collision is possible
Complete Example
================
Here's everything together — pose tracking with regularization and limits:
.. code:: python
import mujoco
from mink import (
Configuration,
ConfigurationLimit,
FrameTask,
PostureTask,
VelocityLimit,
solve_ik,
)
# Load and configure.
model = mujoco.MjModel.from_xml_path("franka_emika_panda/mjx_scene.xml")
configuration = Configuration(model)
configuration.update_from_keyframe("home")
# Primary task: pose tracking.
task = FrameTask(
frame_name="attachment_site",
frame_type="site",
position_cost=1.0,
orientation_cost=1.0,
)
# Regularization: bias toward home configuration.
# This handles both singularities (resists extreme velocities near them)
# and nullspace drift (fills unused DOFs with a preference).
posture_task = PostureTask(model, cost=0.1)
posture_task.set_target_from_configuration(configuration)
tasks = [task, posture_task]
# Limits: joint bounds + velocity cap.
velocity_limits = {"joint1": 2.0, "joint2": 2.0, ...}
limits = [
ConfigurationLimit(model),
VelocityLimit(model, velocity_limits),
]
# IK loop.
dt = 0.01
for _ in range(steps):
task.set_target(get_target()) # Update target each step.
vel = solve_ik(configuration, tasks, dt, "daqp", limits=limits)
configuration.integrate_inplace(vel, dt)
What's Next
===========
For more task types, limits, and real-world examples:
- :doc:`example_gallery` — runnable examples organized by use case
- :doc:`/api/tasks` — all available task types
- :doc:`/api/limits` — all available limits