Week 05 | Day 04

Force Control and Impedance Control: Interacting with the World

Published: April 23, 2026 | Author: Smartotics Learning Journey | Reading Time: 14 min

TL;DR: Position control works in free space, but real robots touch things. Force control regulates contact forces directly. Impedance control shapes the robot’s dynamic relationship between position and force — making it behave like a mass-spring-damper system that adapts to its environment.


Why Position Control Fails at Contact

We’ve spent weeks on position control: “Go to point A, then point B.” This works beautifully in empty space. But what happens when the robot hits something?

Scenario: A robot arm moves toward a surface at position x = 0.5 m.

This is not a bug in the controller. It’s a fundamental mismatch between the control objective (position) and the physical reality (force-limited contact).


Force Control: Direct Force Regulation

The simplest solution: control force instead of position.

Direct Force Control Law

τ = Kf · (F_desired - F_measured)

Where Kf is the force control gain. But this has a critical problem:

In free space, force is always zero. If F_desired = 10N and the robot isn’t touching anything, the controller will accelerate indefinitely trying to generate 10N of contact force.

Better: Force Control with Inner Position Loop

import numpy as np

class ForceController:
    """
    Hybrid force/position controller with inner position loop
    """
    
    def __init__(self, Kp_pos, Kd_pos, Kf, force_axis):
        """
        Args:
            Kp_pos: Position PD gains
            Kd_pos: Velocity damping gains
            Kf: Force control gain
            force_axis: Direction of force control (unit vector)
        """
        self.Kp_pos = np.array(Kp_pos)
        self.Kd_pos = np.array(Kd_pos)
        self.Kf = Kf
        self.force_axis = np.array(force_axis)
        
        # Integral term for force (reduces steady-state error)
        self.force_integral = 0.0
        self.Ki_f = 0.5
    
    def control(self, x, dx, F_measured, x_desired, F_desired, dt):
        """
        Compute control command
        
        Args:
            x: Current position [3]
            dx: Current velocity [3]
            F_measured: Measured contact force (scalar, along force_axis)
            x_desired: Desired position [3]
            F_desired: Desired contact force (scalar)
            dt: Time step
        
        Returns:
            Desired end-effector wrench [6] (force + torque)
        """
        # Position error
        x_err = x_desired - x
        dx_err = -dx
        
        # Force error
        F_err = F_desired - F_measured
        self.force_integral += F_err * dt
        self.force_integral = np.clip(self.force_integral, -10.0, 10.0)  # Anti-windup
        
        # Decompose position error into constrained and free directions
        # Constrained direction (force-controlled): project out force_axis
        pos_free = x_err - np.dot(x_err, self.force_axis) * self.force_axis
        vel_free = dx_err - np.dot(dx_err, self.force_axis) * self.force_axis
        
        # Position control in free directions
        F_pos = self.Kp_pos * pos_free + self.Kd_pos * vel_free
        
        # Force control in constrained direction
        F_force = (self.Kf * F_err + self.Ki_f * self.force_integral) * self.force_axis
        
        # Total force command
        F_cmd = F_pos + F_force
        
        return F_cmd

# Example: Polishing a surface
controller = ForceController(
    Kp_pos=[100.0, 100.0, 100.0],
    Kd_pos=[20.0, 20.0, 20.0],
    Kf=2.0,
    force_axis=[0, 0, 1]  # Control force along Z (normal to surface)
)

# Simulate contact
x = np.array([0.5, 0.3, 0.5])      # Current position
dx = np.array([0.0, 0.0, -0.1])    # Moving toward surface
F_measured = 5.0                    # Already in contact with 5N
x_desired = np.array([0.5, 0.3, 0.5])  # Stay at this XY position
F_desired = 10.0                    # Want 10N contact force

F_cmd = controller.control(x, dx, F_measured, x_desired, F_desired, dt=0.001)
print(f"Force command: {F_cmd}")
print(f"Expected: small XY correction + increased Z push")

Impedance Control: The Gold Standard

Force control regulates force. Impedance control regulates the relationship between force and position. It makes the robot behave like a physical system with programmable mass, spring, and damper properties.

Mechanical Impedance Analogy

Imagine pushing on a robot. With impedance control, it feels like pushing on a virtual mass-spring-damper:

F = M·(ẍ_desired - ẍ) + B·(ẋ_desired - ẋ) + K·(x_desired - x)

Where:

Key insight: The robot doesn’t track position or force directly. It tracks a dynamic relationship between them.

Impedance Control Implementation

class ImpedanceController:
    """
    Cartesian impedance controller for robot end-effector
    """
    
    def __init__(self, M_virtual, B_virtual, K_virtual, null_space_gains=None):
        """
        Args:
            M_virtual: Virtual mass matrix [3x3] or scalar
            B_virtual: Virtual damping matrix [3x3] or scalar
            K_virtual: Virtual stiffness matrix [3x3] or scalar
            null_space_gains: Optional joint-space damping for redundancy
        """
        self.M = np.atleast_1d(M_virtual)
        self.B = np.atleast_1d(B_virtual)
        self.K = np.atleast_1d(K_virtual)
        
        # Ensure matrices
        if self.M.ndim == 1:
            self.M = np.diag(self.M)
        if self.B.ndim == 1:
            self.B = np.diag(self.B)
        if self.K.ndim == 1:
            self.K = np.diag(self.K)
    
    def compute_desired_force(self, x, dx, x_desired, dx_desired, ddx_desired):
        """
        Compute desired end-effector force from impedance law
        
        F = M·(ẍ_d - ẍ) + B·(ẋ_d - ẋ) + K·(x_d - x)
        """
        pos_err = x_desired - x
        vel_err = dx_desired - dx
        accel_err = ddx_desired - np.zeros_like(x)  # We don't measure acceleration directly
        
        F = (self.M @ accel_err + self.B @ vel_err + self.K @ pos_err)
        
        return F
    
    def compute_joint_torques(self, x, dx, x_desired, dx_desired, ddx_desired, 
                             jacobian, dq):
        """
        Convert Cartesian impedance force to joint torques
        
        τ = Jᵀ · F_impedance + (I - Jᵀ·J⁺ᵀ)·τ_null
        """
        # Cartesian impedance force
        F = self.compute_desired_force(x, dx, x_desired, dx_desired, ddx_desired)
        
        # Primary task: impedance
        tau_task = jacobian.T @ F
        
        # Null-space damping (optional, for stability)
        if hasattr(self, 'null_space_gains'):
            J_pinv = np.linalg.pinv(jacobian)
            P = np.eye(len(dq)) - jacobian.T @ J_pinv.T
            tau_null = -self.null_space_gains * dq
            tau_task += P @ tau_null
        
        return tau_task

# Example: Soft interaction controller
soft_controller = ImpedanceController(
    M_virtual=[5.0, 5.0, 5.0],      # Low virtual mass
    B_virtual=[50.0, 50.0, 50.0],   # Moderate damping
    K_virtual=[100.0, 100.0, 100.0] # Low stiffness (compliant)
)

# Example: Stiff precision controller
stiff_controller = ImpedanceController(
    M_virtual=[20.0, 20.0, 20.0],
    B_virtual=[200.0, 200.0, 200.0],
    K_virtual=[2000.0, 2000.0, 2000.0]  # High stiffness (precise)
)

Impedance Control Response


Hybrid Position/Force Control

Many tasks require position control in some directions and force control in others:

TaskPosition-ControlledForce-Controlled
Writing on paperPen tip XY motionPen pressure (Z)
Inserting a pegPeg alignment (XY)Insertion force (Z)
Polishing a surfaceTool path (XY)Normal force (Z)
WalkingFoot placement (XY)Ground reaction (Z)

Selection Matrix Approach

class HybridController:
    """
    Hybrid position/force controller using selection matrices
    """
    
    def __init__(self, S, Kp, Kd, Kf, Ki_f):
        """
        Args:
            S: Selection matrix (1 for force-controlled, 0 for position)
            Kp, Kd: Position control gains
            Kf, Ki_f: Force control gains
        """
        self.S = np.diag(S)  # Force selection
        self.I_S = np.eye(len(S)) - self.S  # Position selection
        
        self.Kp = np.diag(Kp)
        self.Kd = np.diag(Kd)
        self.Kf = Kf
        self.Ki_f = Ki_f
        
        self.force_integral = np.zeros(len(S))
    
    def control(self, x, dx, F, x_des, F_des, dt):
        """
        Compute hybrid control command
        """
        # Position-controlled subspace
        x_err = x_des - x
        dx_err = -dx
        F_pos = self.I_S @ (self.Kp @ x_err + self.Kd @ dx_err)
        
        # Force-controlled subspace
        F_err = F_des - F
        self.force_integral += F_err * dt
        self.force_integral = np.clip(self.force_integral, -5, 5)
        F_force = self.S @ (self.Kf * F_err + self.Ki_f * self.force_integral)
        
        return F_pos + F_force

# Example: Polishing (XY position, Z force)
S = [0, 0, 1]  # Z is force-controlled
hybrid = HybridController(
    S=S,
    Kp=[200, 200, 0],
    Kd=[40, 40, 0],
    Kf=3.0,
    Ki_f=1.0
)

x = np.array([0.5, 0.3, 0.51])
dx = np.array([0.1, 0.05, 0.0])
F = np.array([0, 0, 8.0])
x_des = np.array([0.5, 0.3, 0.5])
F_des = np.array([0, 0, 10.0])

F_cmd = hybrid.control(x, dx, F, x_des, F_des, dt=0.001)
print(f"Hybrid command: {F_cmd}")
print(f"XY: position control, Z: force control")

Admittance Control: The Dual of Impedance

Impedance control: measure position → compute force → command torque Admittance control: measure force → compute position → command position

Admittance control is preferred when:

class AdmittanceController:
    """
    Admittance controller: force input → position output
    """
    
    def __init__(self, M, B, K, dt):
        self.M = M
        self.B = B
        self.K = K
        self.dt = dt
        
        # Virtual state
        self.x_virtual = np.zeros(3)
        self.dx_virtual = np.zeros(3)
    
    def update(self, F_ext, x_desired):
        """
        Update virtual position based on external force
        
        M·ẍ_v + B·ẋ_v + K·(x_v - x_desired) = F_ext
        """
        # Compute virtual acceleration
        pos_err = self.x_virtual - x_desired
        ddx = (F_ext - self.B * self.dx_virtual - self.K * pos_err) / self.M
        
        # Integrate virtual state
        self.dx_virtual += ddx * self.dt
        self.x_virtual += self.dx_virtual * self.dt
        
        return self.x_virtual

# Example: Human-robot collaboration
admittance = AdmittanceController(M=10.0, B=100.0, K=50.0, dt=0.001)

# Human pushes robot with 20N
F_human = np.array([20.0, 0, 0])
x_desired = np.array([0.5, 0.3, 0.4])

# Robot complies and moves
x_compliant = admittance.update(F_human, x_desired)
print(f"Compliant position: {x_compliant}")
print(f"Displacement: {x_compliant - x_desired}")

Choosing the Right Interaction Strategy

ControllerBest ForAvoid When
Position controlFree-space motion, precision tasksContact tasks, uncertain environments
Force controlGrinding, polishing, insertionFree-space motion, fast trajectories
Impedance controlHuman-robot interaction, assemblyVery stiff contact (instability risk)
Admittance controlIndustrial robots with stiff loopsLow-bandwidth force sensing
Hybrid controlTasks with natural position/force decompositionComplex contact geometries

Key Takeaways


Next Steps

Week 5 concludes tomorrow with a comprehensive summary. We’ll review all dynamics concepts, connect them to control strategies, and preview Week 6’s exploration of robot perception and sensing.

Practice Exercise: Implement a complete peg-in-hole insertion simulation using hybrid position/force control. The peg must align in XY (position control) while maintaining 5N insertion force in Z. Add position uncertainty (±2mm) and show that force control in Z prevents jamming while position control in XY achieves alignment.


Disclaimer

For educational purposes only. This article is part of a structured learning curriculum and does not constitute professional engineering advice.