BB.IK.FABRIK
A FABRIK-based inverse kinematics solver for the Beam Bots robotics framework.
FABRIK (Forward And Backward Reaching Inverse Kinematics) is an iterative algorithm that computes joint angles needed to position an end-effector at a target location. It works by alternately reaching from the end-effector toward the target, then from the base back to maintain segment lengths.
Features
- Implements the
BB.IK.Solverbehaviour for pluggable IK solvers - Works with
BB.Robot.Stateor plain position maps - Supports revolute, prismatic, and continuous joints
- Position and orientation solving (quaternion or axis constraints)
- Respects joint limits with optional clamping
- Uses Nx tensors for efficient computation
- Returns best-effort positions even when targets are unreachable
Installation
Add bb_ik_fabrik to your list of dependencies in mix.exs:
def deps do
[
{:bb_ik_fabrik, "~> 0.7.0"}
]
end
Usage
# Define your robot using the BB DSL
robot = MyRobot.robot()
# Create initial state
{:ok, state} = BB.Robot.State.new(robot)
# Define a target position for the end-effector
target = {0.3, 0.2, 0.1}
# Solve inverse kinematics
case BB.IK.FABRIK.solve(robot, state, :end_effector, target) do
{:ok, positions, meta} ->
# Apply the solved positions to the robot state
BB.Robot.State.set_positions(state, positions)
IO.puts("Solved in #{meta.iterations} iterations")
IO.puts("Final distance to target: #{meta.residual}m")
{:error, %BB.Error.Kinematics.Unreachable{residual: residual, positions: positions}} ->
# Target is beyond the robot's reach
IO.puts("Target unreachable, best distance: #{residual}m")
# positions contains best-effort joint values
{:error, %BB.Error.Kinematics.NoSolution{}} ->
IO.puts("Failed to converge within max iterations")
end
Solving with Options
BB.IK.FABRIK.solve(robot, state, :end_effector, target,
max_iterations: 100, # Maximum FABRIK iterations (default: 50)
tolerance: 0.001, # Position tolerance in metres (default: 1.0e-4)
orientation_tolerance: 0.1, # Orientation tolerance in radians (default: 0.01)
respect_limits: true # Clamp to joint limits (default: true)
)
Using solve_and_update/5
For convenience, solve_and_update/5 solves IK and updates the state in one call:
case BB.IK.FABRIK.solve_and_update(robot, state, :end_effector, target) do
{:ok, positions, meta} ->
# State has already been updated
:ok
{:error, %BB.Error.Kinematics.Unreachable{} = error} ->
# State is unchanged on error
{:error, error}
end
Target Formats
Targets can be specified as:
# Position only (Vec3)
target = BB.Math.Vec3.new(0.3, 0.2, 0.1)
# Position with axis constraint ("point tool in this direction")
target = {BB.Math.Vec3.new(0.3, 0.2, 0.1), {:axis, BB.Math.Vec3.new(0.0, 0.0, -1.0)}}
# Position with full orientation (quaternion)
quat = BB.Math.Quaternion.from_axis_angle(BB.Math.Vec3.unit_z(), :math.pi() / 4)
target = {BB.Math.Vec3.new(0.3, 0.2, 0.1), {:quaternion, quat}}
# 4x4 homogeneous transform (extracts both position and orientation)
target = BB.Math.Transform.from_position_quaternion(
BB.Math.Vec3.new(0.3, 0.2, 0.1),
BB.Math.Quaternion.identity()
)
When using orientation constraints, the result metadata includes orientation_residual (in radians) alongside the position residual.
Supported Arm Configurations
FABRIK works best with simple planar or spatial arms where:
- Joints have significant lever arms between them
- The arm has 2-4 degrees of freedom
Works Well
2-link planar arms: Classic shoulder + elbow configuration
3-link arms: Shoulder + elbow + wrist with distinct positions
SCARA-style arms: Horizontal joints with vertical offsets
Simple grippers: Where the end-effector is offset from the last joint
Arms with co-located joints (spherical wrists/shoulders): Each joint is fitted about its own axis, so sharing a point costs nothing
6-DOF anthropomorphic arms (e.g., WidowX, Kinova): position targets solve to tolerance; full pose targets are the hardest case and converge less often
How a target is solved
Classic FABRIK moves points as though every joint were a ball joint, so the configuration it settles on generally has no counterpart in any pose the robot can hold. For a position target this implementation keeps FABRIK's backward reach but treats its answer as desired directions: the forward pass walks base to tip choosing, for each joint, the rotation about its real world axis that carries its links closest to those directions, clamped to its limits, and regenerates the positions beyond it by forward kinematics.
Every pose considered is therefore one the robot can actually hold, and the joint values are the solver's output rather than something recovered from a point cloud afterwards.
Orientation rides the same fit: three points sit rigidly on the target link's axes, and putting them where they are wanted is the same as pointing the frame where it is wanted.
Limitations
- Serial chains only: Does not support branching topologies
- Convergence is not guaranteed: a minority of solves exhaust
:max_iterationsrather than reaching tolerance, and return best-effort positions withreached: false - Cost: constraining costs iterations — tens rather than a handful, each one a
forward-kinematics pass per joint. Batch with
Nx.vectorize/2when solving many chains, such as the legs of a gait - Collinear targets: FABRIK can struggle when the target lies on the same line as a straight chain
When to Use a Different Solver
Consider analytical IK or Jacobian-based methods when:
- You have a 6-DOF arm with specific geometry (closed-form solutions exist)
- You need precise control over which joints move
- You need to optimise for specific joint configurations (elbow up vs down)
- You require guaranteed orientation accuracy for complex arm geometries
Documentation
Full documentation is available at HexDocs.