# Tutorial 5: End-Effector Control — IK & PoseAction
EE Pick & Drop
|
Mobile Manipulator
|
**Source files:**
- [`examples/arm/pick_drop_arm_ee_demo.py`](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/pick_drop_arm_ee_demo.py) — low-level callback approach
- [`examples/arm/pick_drop_arm_ee_action_demo.py`](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/pick_drop_arm_ee_action_demo.py) — action-queue approach
This tutorial shows how to control a robot arm by specifying **end-effector (EE) positions**
instead of joint angles. PyBulletFleet's built-in IK solver converts Cartesian targets
to joint angles automatically.
**What you'll learn:**
- Using `move_end_effector()` for direct EE position control
- `PoseAction` — an action that moves the EE via IK
- Using `ee_target_position` in `PickAction` / `DropAction`
- Tuning IK behaviour with `IKParams`
- Orientation control with `target_orientation`
- **Mobile manipulator IK** — auto-detection, wheel locking, `ik_joint_names`
- **`drop_relative_pose`** — dropping objects relative to the EE position
**Prerequisites:** [Tutorial 4 — Arm Joint Control](arm-pick-drop) (joint targets, `use_fixed_base`, link attachment).
---
## 1. Why EE Position Control?
[Tutorial 4](arm-pick-drop) uses `JointAction` to specify exact joint angles for pick/drop positions.
This works, but requires the user to **pre-compute** the joint angles for every target position.
EE position control inverts the problem: you specify **where the end-effector should go**
(in world coordinates), and the IK solver figures out the joint angles.
| Approach | You specify | Library computes |
|----------|------------|-----------------|
| `JointAction` | Joint angles `[1.5, 1.5, 1.5, 0.0]` | — |
| `PoseAction` / `move_end_effector` | EE position `[0.0, -0.3, 0.3]` | Joint angles via IK |
---
## 2. Direct EE Control: `move_end_effector()`
The simplest way to command the EE:
```python
# Move end-effector to position [x, y, z] in world frame
reachable = arm_agent.move_end_effector([0.0, -0.3, 0.3])
if reachable:
print("IK solution found — joints moving to target")
else:
print("Target unreachable — best-effort joint targets set")
```
`move_end_effector()` internally:
1. Calls `_solve_ik()` — multi-seed iterative IK solver
2. Checks reachability (`_check_ik_reachability()`)
3. Sets joint targets via `set_all_joints_targets()`
4. Returns `True` if the IK solution places the EE within tolerance
After calling `move_end_effector()`, joints move toward the solution over subsequent
`update()` steps — just like `set_all_joints_targets()`.
### With Orientation
To control the EE orientation as well as position, pass a quaternion `[x, y, z, w]`:
```python
import pybullet as p
# Downward-pointing orientation (Z- axis)
orn = list(p.getQuaternionFromEuler([0, 3.14, 0]))
reachable = arm_agent.move_end_effector(
[0.0, -0.3, 0.3],
target_orientation=orn,
)
```
### Checking Convergence
Use `are_joints_at_targets()` (no arguments) to check if all joints have reached
the last commanded targets from `move_end_effector()`:
```python
if arm_agent.are_joints_at_targets(tolerance=0.05):
print("EE has reached the target position")
```
---
## 3. PoseAction — EE Control in the Action Queue
`PoseAction` wraps `move_end_effector()` as an Action for use in action sequences:
```python
from pybullet_fleet.action import PoseAction
actions = [
PoseAction(target_position=[0.0, -0.3, 0.3], tolerance=0.02),
PoseAction(target_position=[0.0, 0.3, 0.3], tolerance=0.02),
PoseAction(target_position=[0.0, 0.0, 0.75]), # home
]
arm_agent.add_action_sequence(actions)
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `target_position` | `list[float]` | required | EE target `[x, y, z]` in world frame |
| `target_orientation` | `list[float]` | `None` | Quaternion `[x, y, z, w]`; `None` = position only |
| `end_effector_link` | `int \| str \| None` | `None` | EE link index/name; `None` = last link |
| `tolerance` | `float` | `0.02` | EE Cartesian distance tolerance (metres) |
| `max_force` | `float` | `500.0` | Motor force for physics mode |
**Completion:** Joints within default joint tolerance of the IK solution **and** EE within `tolerance` of the target position.
**Unreachable targets:** When the IK solver determines the target is unreachable,
`PoseAction` does **not** fail immediately. Instead:
1. A best-effort IK solution is computed and joint targets are set
2. Joints move toward the best-effort targets and settle
3. After settling, the action completes with `ActionStatus.FAILED`
4. An error message is logged: `"IK target was not reachable"`
This means the action queue **does not stall** — the `FAILED` status propagates
to the next action in the sequence. To handle failures explicitly, check the
action's status after completion:
```python
action = PoseAction(target_position=[100, 100, 100]) # unreachable
# ... after execution ...
if action.status == ActionStatus.FAILED:
print(f"Failed: {action.error_message}")
```
> **`move_end_effector()` behaviour:** Returns `False` for unreachable targets
> but still sets joint targets (best-effort). The same applies to
> `PickAction(ee_target_position=...)` and `DropAction(ee_target_position=...)`.
---
## 4. EE-Based Pick & Drop
`PickAction` and `DropAction` accept `ee_target_position` as an alternative to
joint-level positioning. When set, the action uses IK to move the EE to the
target position before picking/dropping
(see [`pick_drop_arm_ee_action_demo.py`](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/pick_drop_arm_ee_action_demo.py)):
```python
from pybullet_fleet.action import PickAction, DropAction, PoseAction, WaitAction
EE_LINK = arm_agent.get_num_joints() - 1
OFFSET = Pose.from_xyz(0, 0, 0.14) # box offset from EE
actions = [
# Pick: move EE to box position via IK, then attach
PickAction(
target_object_id=box.body_id,
use_approach=False,
ee_target_position=[0.0, -0.3, 0.3],
attach_link=EE_LINK,
attach_relative_pose=OFFSET,
),
# Drop: move EE to drop position via IK, then detach
DropAction(
drop_pose=Pose.from_xyz(0.0, 0.3, 0.1),
use_approach=False,
ee_target_position=[0.0, 0.3, 0.3],
),
# Return home
PoseAction(target_position=[0.0, 0.0, 0.75]),
WaitAction(duration=0.5),
]
arm_agent.add_action_sequence(actions)
```
**Key difference from Tutorial 4:**
| Tutorial 4 (joint targets) | Tutorial 5 (EE position) |
|----------------------------|--------------------------|
| `JointAction(target_joint_positions=[...])` | `PoseAction(target_position=[x, y, z])` |
| `PickAction(use_approach=False)` + separate `JointAction` | `PickAction(ee_target_position=[x, y, z])` |
| User computes joint angles | IK solver computes angles |
---
## 5. Tuning IK with `IKParams`
The IK solver uses a multi-seed iterative strategy. You can tune its behaviour
by passing an `IKParams` dataclass to `Agent.from_urdf()`:
```python
from pybullet_fleet.agent import Agent, IKParams
ik_cfg = IKParams(
max_outer_iterations=5, # refinement iterations per seed (default: 5)
convergence_threshold=0.01, # position error threshold in metres (default: 0.01)
max_inner_iterations=200, # PyBullet IK iterations per attempt (default: 200)
residual_threshold=1e-4, # IK residual threshold (default: 1e-4)
reachability_tolerance=0.02,# tolerance for reachability check in metres (default: 0.02)
seed_quartiles=(0.25, 0.5, 0.75),# joint range quartiles for seed generation (default)
)
arm_agent = Agent.from_urdf(
urdf_path="robots/arm_robot.urdf",
pose=Pose.from_xyz(0, 0, 0),
use_fixed_base=True,
sim_core=sim_core,
ik_params=ik_cfg,
)
```
The solver tries the following seed strategies (each refined up to
`max_outer_iterations` times):
1. **Current joint positions** — works well when EE is near the target
2. **Quartile seeds** — joint-range fractions in `seed_quartiles` (default
25 %, 50 %, 75 %) for diversified exploration across the joint space
For most use cases, the defaults work well. Increase `max_outer_iterations` if
the solver fails to converge for targets requiring large joint displacements.
---
## 6. Mobile Manipulator IK
On composite robots (mobile base + arm), the IK solver must ignore the base joints
and only solve for arm joints. PyBulletFleet handles this automatically:
- **FIXED joints** are skipped (PyBullet excludes them from IK)
- **Continuous joints** (lower limit ≥ upper limit, e.g. drive wheels) are **locked** at their current positions
This means `move_end_effector()` works on a mobile manipulator without extra
configuration — the wheels stay put while the arm moves to reach the target.
### Explicit control with `ik_joint_names`
For cases where auto-detection isn't enough, use `ik_joint_names` to explicitly
list the joints the IK solver is allowed to move:
```python
from pybullet_fleet.agent import Agent, IKParams
ik_cfg = IKParams(
ik_joint_names=(
"mount_to_shoulder",
"shoulder_to_elbow",
"elbow_to_wrist",
"wrist_to_end",
),
)
agent = Agent.from_urdf(
urdf_path="robots/mobile_manipulator.urdf",
pose=Pose.from_xyz(0, 0, 0),
sim_core=sim_core,
ik_params=ik_cfg,
mass=0.0, # kinematic mode
)
```
All movable joints **not** in `ik_joint_names` are locked at their current positions.
When `ik_joint_names` is `None` (default), auto-detection is used.
See [`examples/arm/mobile_manipulator_demo.py`](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/mobile_manipulator_demo.py)
for a full mobile manipulator demo with IK-based pick/drop.
### `drop_relative_pose` — Relative Drop Positioning
When an object is attached to the EE of a mobile manipulator, you often don't know
the exact world position where it should be dropped (since the base can be anywhere).
Use `drop_relative_pose` to specify an offset from the object's current position:
```python
from pybullet_fleet.action import DropAction
# Drop the object exactly where the EE currently is (no offset)
drop = DropAction(
drop_pose=Pose.from_xyz(0, 0, 0), # base navigation target (still used for approach/move-to-drop)
drop_relative_pose=Pose.from_xyz(0, 0, 0), # (0,0,0) = release in place
use_approach=False,
)
# Drop with a small forward offset from EE
drop = DropAction(
drop_pose=Pose.from_xyz(0, 0, 0), # base navigation target
drop_relative_pose=Pose.from_xyz(0.1, 0, 0), # 10cm forward
use_approach=False,
)
```
When `drop_relative_pose` is set, the object's final position is computed as:
*current_object_pose* × *drop_relative_pose* — instead of teleporting to `drop_pose`.
---
## 7. Low-Level Callback Approach
For full control, use `move_end_effector()` in a state-machine callback
(see `pick_drop_arm_ee_demo.py`):
```python
step_state = 0
_ee_set = False
def pick_drop_callback(sim_core, dt):
global step_state, _ee_set
def _move_ee_once(pos):
global _ee_set
if not _ee_set:
arm_agent.move_end_effector(pos)
_ee_set = True
def _at_target():
return arm_agent.are_joints_at_targets(tolerance=0.05)
def _advance(next_state):
global step_state, _ee_set
step_state = next_state
_ee_set = False
if step_state == 0: # Move to pick position
_move_ee_once([0.0, -0.3, 0.3])
if _at_target():
_advance(1)
elif step_state == 1: # Pick
arm_agent.attach_object(box, parent_link_index=EE_LINK,
relative_pose=Pose.from_xyz(0, 0, 0.14))
_advance(2)
elif step_state == 2: # Move to drop position
_move_ee_once([0.0, 0.3, 0.3])
if _at_target():
_advance(3)
elif step_state == 3: # Drop
arm_agent.detach_object(box)
_advance(0)
sim_core.register_callback(pick_drop_callback, frequency=10)
```
---
## 8. Running the Demos
```bash
# Action-queue approach (recommended)
python examples/arm/pick_drop_arm_ee_action_demo.py
# Low-level callback approach
python examples/arm/pick_drop_arm_ee_demo.py
# Rail arm — prismatic (linear) + revolute joints with EE control
python examples/arm/rail_arm_demo.py
# Mobile manipulator (base + arm) — kinematic IK pick/drop
python examples/arm/mobile_manipulator_demo.py
```
Both single-arm EE demos use kinematic mode for fast execution.
### Switching Robot Models
All EE demos accept a `--robot` argument to swap the arm model.
Pass an arm model name or a direct URDF path:
```bash
python examples/arm/pick_drop_arm_ee_action_demo.py --robot kuka_iiwa
python examples/arm/pick_drop_arm_ee_demo.py --robot arm_robot
```
| `--robot` default | Alternatives |
|-------------------|-------------|
| `panda` | `kuka_iiwa`, `arm_robot` |
See [Tutorial 6 — Robot Models](robot-models) for the full model resolution system.
---
## 9. Prismatic Joints & Rail Arms
The IK solver works with **prismatic (linear) joints** out of the box. A prismatic
joint in the kinematic chain lets the IK solver adjust both the linear position
(e.g., rail height) and revolute angles to reach the target.
[`examples/arm/rail_arm_demo.py`](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/rail_arm_demo.py)
demonstrates a **rail arm** (1 prismatic Z-axis + 4 revolute = 5 DOF) picking a box
from a high shelf and placing it at a low position:
```python
agent = Agent.from_urdf(
urdf_path="robots/rail_arm_robot.urdf",
pose=Pose.from_xyz(0, 0, 0),
use_fixed_base=True,
sim_core=sim_core,
)
# EE control — IK decides optimal rail height + arm configuration
actions = [
PickAction(
target_object_id=box.body_id,
use_approach=False,
ee_target_position=[0.1, -0.3, 1.0], # high shelf
attach_link=EE_LINK,
attach_relative_pose=Pose.from_xyz(0, 0, 0.14),
),
DropAction(
drop_pose=Pose.from_xyz(0.1, 0.3, 0.3), # low position
use_approach=False,
ee_target_position=[0.1, 0.3, 0.3],
),
]
```
The demo also shows **per-joint tolerance** via dict — tightening prismatic accuracy
(±5 mm) while keeping revolute joints looser (±0.05 rad):
```python
JointAction(
target_joint_positions={"rail_joint": 0.0, "base_to_shoulder": 0.0, ...},
tolerance={"rail_joint": 0.005, "base_to_shoulder": 0.05, ...},
)
```
See [Tutorial 4 — JointAction Tolerance](arm-pick-drop) for the full tolerance
reference (scalar, dict, agent-level fallback).
---
## API Summary
| API | Description |
|-----|-------------|
| `agent.move_end_effector(pos, orientation)` | Direct EE position command via IK |
| `agent.are_joints_at_targets(tolerance)` | Check convergence to last targets |
| `agent._solve_ik(pos, orientation, ee_link)` | Low-level IK solver (internal) |
| `PoseAction(target_position, orientation, ...)` | EE control as an Action |
| `PickAction(ee_target_position=...)` | IK-based pick positioning |
| `DropAction(ee_target_position=...)` | IK-based drop positioning |
| `IKParams(...)` | IK solver tuning parameters |
| `IKParams(ik_joint_names=(...))` | Restrict IK to specific joints |
| `DropAction(drop_relative_pose=...)` | Drop relative to current EE position |
---
## See Also
- [Tutorial 4 — Arm Joint Control](arm-pick-drop): `JointAction`, joint-level pick/drop, tolerance reference
- [Tutorial 2 — Action System](action-system): mobile robot pick/drop with `MoveAction`
- [Rail Arm Demo](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/rail_arm_demo.py): prismatic + revolute EE control
- [Mobile Manipulator Demo](https://github.com/yuokamoto/PyBulletFleet/blob/main/examples/arm/mobile_manipulator_demo.py): kinematic mobile manipulator with IK pick/drop
- [Architecture Overview](../architecture/overview): IK internals, joint control modes