Chapter 4: Mobile Robot Dynamics (Applied)

Lesson 4: Stability and Tip-Over Risk (vehicle view)

This lesson develops a vehicle-centric stability analysis for ground mobile robots, focusing on roll/pitch tip-over risk under turning and acceleration. We derive quasi-static tipping thresholds, introduce load-transfer metrics used in vehicle dynamics, and connect them to friction-limited sliding. The goal is a mathematically grounded “risk evaluator” that can be embedded into speed planning and safety monitors without introducing concepts beyond the applied dynamics models established in Lessons 1–3.

1. Conceptual Overview: What “Tip-Over” Means in Vehicle Dynamics

We assume a ground robot moving on a locally planar surface, with a finite set of wheel/track contact points. Two failure modes compete during aggressive maneuvers:

  • Sliding: tangential tire–ground forces saturate (Coulomb limit), causing lateral/longitudinal slip.
  • Tip-over: the normal force at at least one contact line goes to zero, so the support can no longer generate a restoring moment.

Let the robot’s center of gravity (CoG) position in the body frame be \( \mathbf{r}_G = [x_G,\, y_G,\, h]^\top \) with height \( h > 0 \) above the ground plane. Let the planar acceleration of the CoG be \( \mathbf{a}_{xy} = [a_x,\, a_y]^\top \). In a quasi-static accelerating frame attached to the robot, the inertial (pseudo) force is \( \mathbf{F}_I = -m[a_x,\, a_y,\, 0]^\top \) and gravity is \( \mathbf{F}_g = m[0,\,0,\,-g]^\top \). The combined effective specific force direction is therefore proportional to \( \mathbf{g}_{\mathrm{eff}} = [-a_x,\,-a_y,\,-g]^\top \).

A classic stability criterion is: the line of action of the resultant external force must intersect the support polygon. For a flat ground plane \( z=0 \), consider the line \( \ell(\lambda)=\mathbf{r}_G+\lambda \mathbf{g}_{\mathrm{eff}} \). Its intersection with \( z=0 \) occurs at \( \lambda^\star = h/g \), yielding the “resultant-footpoint”

\[ \mathbf{p}^\star = \begin{bmatrix} x_G - \dfrac{h}{g}a_x \\ y_G - \dfrac{h}{g}a_y \end{bmatrix}. \]

Let \( \mathcal{S} \subset \mathbb{R}^2 \) be the support polygon (convex hull of contact points projected to the ground). A quasi-static non-tip condition is \( \mathbf{p}^\star \in \mathcal{S} \). When \( \mathbf{p}^\star \) crosses an edge, the normal reaction at the opposite side reaches zero and the vehicle begins to roll/pitch about that edge.

flowchart TD
  A["Inputs: track T, wheelbase L, CoG height h, friction mu"] --> B["Trajectory: v(t), curvature kappa(t), accel a_x(t)"]
  B --> C["Compute: a_y(t)=v^2*kappa, a_planar=sqrt(a_x^2+a_y^2)"]
  C --> D["Tip metrics: LTR_y=2*h*a_y/(g*T), LTR_x=2*h*a_x/(g*L)"]
  D --> E["Sliding metric: a_planar <= mu*g ?"]
  E --> F["Risk output: margins, safe speed bound, alarms"]
        

2. Quasi-Static Lateral Tip-Over Threshold (Vehicle View)

Consider a symmetric four-wheel (or two-track-line) vehicle with track width \( T \) (left–right spacing). Let \( b = T/2 \). Let total vertical reactions be \( F_{zL} \) (left) and \( F_{zR} \) (right), with \( F_{zL}+F_{zR}=mg \) under flat ground and negligible vertical acceleration. Under a lateral acceleration \( a_y \), the inertial force magnitude is \( m|a_y| \) acting at height \( h \), generating a roll moment about the longitudinal axis.

Proposition 1 (quasi-static lateral tipping threshold). Under rigid-body quasi-static roll (no suspension compliance), tip-over onset occurs when \( |a_y| = g\,\dfrac{T}{2h} \).

Proof (moment balance about the vehicle centerline). Take moments about the longitudinal axis through the CoG projection on the ground. The difference in vertical reactions creates a restoring moment: \( (F_{zR}-F_{zL})\,b \). The lateral inertial force creates an overturning moment: \( m a_y h \). Quasi-static equilibrium requires

\[ (F_{zR}-F_{zL})\,b = m a_y h. \]

Divide by \( mg \) and use \( F_{zR}+F_{zL}=mg \). Define the Load Transfer Ratio (LTR) for lateral motion:

\[ \mathrm{LTR}_y \triangleq \frac{F_{zR}-F_{zL}}{F_{zR}+F_{zL}} = \frac{m a_y h/b}{mg} = \frac{h a_y}{b g} = \frac{2h}{gT}a_y. \]

Tip-over begins when the inside reaction becomes zero, e.g., \( F_{zL}=0 \) (turning such that left unloads), which implies \( F_{zR}=mg \) and thus \( \mathrm{LTR}_y=1 \). Therefore \( |\mathrm{LTR}_y|=1 \) at onset, giving

\[ |\mathrm{LTR}_y| = \left|\frac{2h}{gT}a_y\right| = 1 \;\Rightarrow\; |a_y| = g\,\frac{T}{2h}. \]

This matches the support-polygon criterion from Section 1: for a centered CoG (\( y_G=0 \)), the resultant-footpoint is \( y^\star = -(h/g)a_y \), and the no-tip condition \( |y^\star| \le b \) becomes exactly \( |a_y| \le g\,b/h \). ∎

3. Quasi-Static Pitch Tip-Over Under Acceleration/Braking

The same derivation applies in the longitudinal direction with wheelbase \( L \) and half-wheelbase \( \ell=L/2 \) (for a centered CoG along the wheelbase; if the CoG is not centered, replace \( \ell \) by the appropriate front/rear lever arm). Let \( F_{zF} \) and \( F_{zR} \) be total front and rear normal loads. Define the longitudinal LTR:

\[ \mathrm{LTR}_x \triangleq \frac{F_{zF}-F_{zR}}{F_{zF}+F_{zR}} = \frac{2h}{gL}a_x. \]

Pitch tip-over (nose-over in hard braking or tail-over in extreme acceleration for short wheelbase robots) occurs when \( |\mathrm{LTR}_x|=1 \), i.e.

\[ |a_x| = g\,\frac{L}{2h}. \]

For most ground AMRs, longitudinal tip-over is less likely than lateral tip-over because wheelbase is often comparable to or larger than track, and typical motion planners avoid extremely high \( |a_x| \). However, compact robots with high payload stacks (large \( h \)) and abrupt velocity commands can violate this bound.

4. Sliding vs Tip-Over: A Rigorous Comparison

From Lesson 2 (slip/traction framing), a simplified Coulomb bound for planar acceleration magnitude is \( \|\mathbf{a}_{xy}\| \le \mu g \). For a pure steady turn with negligible \( a_x \), sliding threshold is \( |a_y| \le \mu g \). Tip-over threshold from Section 2 is \( |a_y| \le g\,\dfrac{T}{2h} \).

Design inequality (tip-first vs slide-first). Tip-over occurs before sliding if the tipping threshold is smaller than the friction threshold:

\[ g\,\frac{T}{2h} \le \mu g \;\Rightarrow\; \frac{T}{2h} \le \mu. \]

Interpretation: a “tall and narrow” robot (small \( T/(2h) \)) tips easily; a “wide and low” robot tends to slide first. This inequality is central for AMR mechanical design (payload placement), speed limiting, and safety certification arguments.

In trajectory-following, a common curvature model is \( a_y(t)=v(t)^2\kappa(t) \), where speed is \( v(t) \) and curvature is \( \kappa(t) \). Enforcing tipping and sliding limits yields speed bounds:

\[ v(t) \le \sqrt{\frac{gT}{2h|\kappa(t)|}} \quad \text{and} \quad v(t) \le \sqrt{\frac{\mu g}{|\kappa(t)|}}. \]

A conservative safe speed is therefore \( v_{\mathrm{safe}}(t)=\min\!\left(\sqrt{\dfrac{gT}{2h|\kappa(t)|}},\,\sqrt{\dfrac{\mu g}{|\kappa(t)|}}\right) \). These formulas are directly implementable in planners and safety monitors, and they match the workflow in the diagram of Section 1.

5. Roll Compliance: A Minimal Dynamic Extension (Still Vehicle-Centric)

Many AMRs have non-negligible compliance (tires, suspensions, chassis flex), so roll angle \( \phi \) can amplify load transfer beyond the rigid-body quasi-static estimate. A minimal roll dynamics model is a second-order ODE:

\[ I_\phi \ddot{\phi} + c_\phi \dot{\phi} + k_\phi \phi = m h a_y, \]

where \( I_\phi \) is roll inertia about the longitudinal axis, \( c_\phi \) is roll damping, and \( k_\phi \) is roll stiffness. The transfer function is:

\[ \frac{\Phi(s)}{A_y(s)} = \frac{m h}{I_\phi s^2 + c_\phi s + k_\phi}. \]

A simple load-transfer augmentation uses the fact that roll shifts the CoG laterally by approximately \( h\sin\phi \). The gravitational moment term becomes \( m g h \sin\phi \), so the quasi-static moment balance generalizes to:

\[ (F_{zR}-F_{zL})\,b = m a_y h + m g h \sin\phi. \]

Dividing by \( mg \) and using \( F_{zR}+F_{zL}=mg \) yields a roll-augmented LTR model:

\[ \mathrm{LTR}_y \approx \frac{2h}{gT}a_y + \frac{2h}{T}\sin\phi. \]

For small roll angles (\( \sin\phi \approx \phi \)), and for constant curvature turns where \( a_y \) is (approximately) constant, the steady-state roll is \( \phi_{ss}=\dfrac{m h}{k_\phi}a_y \). Substituting gives a steady-state LTR amplification factor:

\[ \mathrm{LTR}_{y,ss} \approx \frac{2h}{gT}a_y\left(1+\frac{m g h}{k_\phi}\right). \]

Therefore, finite compliance effectively reduces the allowable \( |a_y| \) before reaching \( |\mathrm{LTR}_y|=1 \). This is why “vehicle view” stability assessments often include conservative margins (e.g., alert at \( |\mathrm{LTR}_y| \ge 0.7 \)) and rate-based logic (watching \( \dot{\phi} \) or \( \dot{\mathrm{LTR}} \)) in real systems.

6. Practical Tip-Over Risk Indices (What You Compute Online)

A simple, physically interpretable stability margin is: \( m_y(t)=1-|\mathrm{LTR}_y(t)| \). Tip-over onset corresponds to \( m_y(t)=0 \). Similarly define \( m_x(t)=1-|\mathrm{LTR}_x(t)| \).

For sliding, a conservative margin (planar) is: \( m_\mu(t)=\mu g - \|\mathbf{a}_{xy}(t)\| \). Online monitoring then becomes:

\[ \text{safe if } m_y(t) > 0,\; m_x(t) > 0,\; m_\mu(t) > 0 \quad \text{and risky as any margin approaches } 0. \]

flowchart TD
  S["Start cycle (planner or safety node)"] --> P["Read params: T, L, h, mu, g"]
  P --> Q["Estimate curvature kappa and speed v"]
  Q --> AY["Compute a_y = v^2*kappa"]
  Q --> AX["Estimate a_x from dv/dt"]
  AY --> LTRY["LTR_y = 2*h*a_y/(g*T)"]
  AX --> LTRX["LTR_x = 2*h*a_x/(g*L)"]
  LTRY --> M1["m_y = 1-abs(LTR_y)"]
  LTRX --> M2["m_x = 1-abs(LTR_x)"]
  M1 --> MU["m_mu = mu*g - sqrt(a_x^2+a_y^2)"]
  M2 --> MU
  MU --> D["If any margin near 0: reduce speed / replan curvature"]
  D --> S
        

7. Implementations: Tip-Over & Sliding Checks in Multiple Languages

The following implementations compute \( \mathrm{LTR}_y \), \( \mathrm{LTR}_x \), and sliding margins along a trajectory. In real AMR software, you typically:

  • Estimate curvature \( \kappa \) from the local path (e.g., spline fit or discrete curvature).
  • Use commanded or measured speed \( v \) and estimate \( a_x \) from finite differences with filtering.
  • Publish a conservative speed limit \( v_{\mathrm{safe}} \) to the local planner or safety supervisor.

7.1 Python

File: Chapter4_Lesson4.py

# Chapter4_Lesson4.py
# Stability and Tip-Over Risk (vehicle view) — reference implementation
#
# This script computes:
#   - LTR_y (lateral load transfer ratio) and LTR_x (longitudinal load transfer ratio)
#   - stability margins m_y = 1 - |LTR_y|, m_x = 1 - |LTR_x|
#   - friction (sliding) margin m_mu = mu*g - sqrt(a_x^2 + a_y^2)
#   - curvature-based safe speed limits v_tip, v_slide, v_safe
#
# Dependencies: numpy (required), matplotlib (optional for plotting)

from __future__ import annotations
from dataclasses import dataclass
from typing import Tuple
import numpy as np


@dataclass(frozen=True)
class VehicleParams:
    T: float      # track width [m]
    L: float      # wheelbase [m]
    h: float      # CoG height [m]
    mu: float     # friction coefficient [-]
    g: float = 9.81


def ltr_y(a_y: np.ndarray, p: VehicleParams) -> np.ndarray:
    """Lateral load transfer ratio: LTR_y = 2 h a_y / (g T)."""
    return (2.0 * p.h * a_y) / (p.g * p.T)


def ltr_x(a_x: np.ndarray, p: VehicleParams) -> np.ndarray:
    """Longitudinal load transfer ratio: LTR_x = 2 h a_x / (g L)."""
    return (2.0 * p.h * a_x) / (p.g * p.L)


def margins(a_x: np.ndarray, a_y: np.ndarray, p: VehicleParams) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
    """Return (m_y, m_x, m_mu) stability margins."""
    LTRy = ltr_y(a_y, p)
    LTRx = ltr_x(a_x, p)
    m_y = 1.0 - np.abs(LTRy)
    m_x = 1.0 - np.abs(LTRx)
    a_planar = np.sqrt(a_x**2 + a_y**2)
    m_mu = p.mu * p.g - a_planar
    return m_y, m_x, m_mu


def safe_speed_from_curvature(kappa: np.ndarray, p: VehicleParams, v_cap: float = 10.0) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
    """
    For (possibly time-varying) curvature kappa(t), compute conservative speed bounds:
      v_tip(t)  = sqrt(g T / (2 h |kappa|))
      v_slide(t)= sqrt(mu g / |kappa|)
      v_safe(t) = min(v_tip, v_slide, v_cap)
    For kappa == 0, the bound is v_cap.
    """
    k = np.abs(kappa)
    eps = 1e-12
    v_tip = np.sqrt((p.g * p.T) / (2.0 * p.h * np.maximum(k, eps)))
    v_slide = np.sqrt((p.mu * p.g) / np.maximum(k, eps))
    # If curvature is effectively zero, set to cap
    v_tip = np.where(k < 1e-9, v_cap, v_tip)
    v_slide = np.where(k < 1e-9, v_cap, v_slide)
    v_safe = np.minimum(np.minimum(v_tip, v_slide), v_cap)
    return v_tip, v_slide, v_safe


def demo():
    # Example parameters for a small AMR
    p = VehicleParams(T=0.60, L=0.80, h=0.30, mu=0.70)

    # Synthetic trajectory signals (time, curvature, commanded speed)
    t = np.linspace(0.0, 10.0, 501)
    kappa = 0.4 * np.sin(0.6 * t)   # [1/m], alternating left/right
    v_cmd = 1.8 + 0.6 * np.sin(0.3 * t + 0.7)  # [m/s]

    # Curvature-based lateral acceleration model (steady turn approximation)
    a_y = v_cmd**2 * kappa

    # Longitudinal acceleration (finite difference)
    dt = t[1] - t[0]
    a_x = np.gradient(v_cmd, dt)

    # Compute margins
    m_y, m_x, m_mu = margins(a_x, a_y, p)

    # Compute speed bounds (e.g. planner should enforce v_cmd <= v_safe)
    v_tip, v_slide, v_safe = safe_speed_from_curvature(kappa, p, v_cap=3.0)

    # Report worst-case margins
    print("Worst (minimum) margins over trajectory:")
    print(f"  min m_y  (roll tip): {m_y.min(): .3f}")
    print(f"  min m_x  (pitch tip): {m_x.min(): .3f}")
    print(f"  min m_mu (sliding): {m_mu.min(): .3f}")
    print("Worst speed limit (minimum v_safe):", float(v_safe.min()))

    # Optional plotting
    try:
        import matplotlib.pyplot as plt
        plt.figure()
        plt.plot(t, v_cmd, label="v_cmd")
        plt.plot(t, v_safe, label="v_safe")
        plt.xlabel("t [s]")
        plt.ylabel("speed [m/s]")
        plt.legend()
        plt.grid(True)

        plt.figure()
        plt.plot(t, ltr_y(a_y, p), label="LTR_y")
        plt.plot(t, ltr_x(a_x, p), label="LTR_x")
        plt.xlabel("t [s]")
        plt.ylabel("LTR [-]")
        plt.legend()
        plt.grid(True)

        plt.show()
    except Exception as e:
        print("Plotting skipped:", e)


if __name__ == "__main__":
    demo()

7.2 C++

File: Chapter4_Lesson4.cpp

// Chapter4_Lesson4.cpp
// Stability and Tip-Over Risk (vehicle view) — reference implementation (no ROS dependencies)
//
// Build example:
//   g++ -O2 -std=c++17 Chapter4_Lesson4.cpp -o Chapter4_Lesson4
//
// This program computes:
//   - LTR_y = 2 h a_y / (g T)
//   - LTR_x = 2 h a_x / (g L)
//   - margins m_y = 1 - |LTR_y|, m_x = 1 - |LTR_x|
//   - friction margin m_mu = mu*g - sqrt(a_x^2 + a_y^2)
//   - curvature-based safe speeds v_tip, v_slide, v_safe

#include <cmath>
#include <iomanip>
#include <iostream>
#include <vector>
#include <algorithm>

struct VehicleParams {
    double T;   // track width [m]
    double L;   // wheelbase [m]
    double h;   // CoG height [m]
    double mu;  // friction coefficient [-]
    double g;   // gravity [m/s^2]
};

static double ltr_y(double a_y, const VehicleParams& p) {
    return (2.0 * p.h * a_y) / (p.g * p.T);
}

static double ltr_x(double a_x, const VehicleParams& p) {
    return (2.0 * p.h * a_x) / (p.g * p.L);
}

static void safe_speed_from_curvature(double kappa, const VehicleParams& p, double v_cap,
                                      double& v_tip, double& v_slide, double& v_safe) {
    const double k = std::abs(kappa);
    const double eps = 1e-12;

    if (k < 1e-9) {
        v_tip = v_cap;
        v_slide = v_cap;
        v_safe = v_cap;
        return;
    }

    v_tip = std::sqrt((p.g * p.T) / (2.0 * p.h * std::max(k, eps)));
    v_slide = std::sqrt((p.mu * p.g) / std::max(k, eps));
    v_safe = std::min({v_tip, v_slide, v_cap});
}

int main() {
    VehicleParams p{0.60, 0.80, 0.30, 0.70, 9.81};

    // Synthetic signals
    const int N = 501;
    const double t0 = 0.0, t1 = 10.0;
    const double dt = (t1 - t0) / (N - 1);

    std::vector<double> t(N), kappa(N), v_cmd(N), a_x(N), a_y(N);
    for (int i = 0; i < N; ++i) {
        t[i] = t0 + i * dt;
        kappa[i] = 0.4 * std::sin(0.6 * t[i]);
        v_cmd[i] = 1.8 + 0.6 * std::sin(0.3 * t[i] + 0.7);
    }

    // a_x via finite difference; a_y = v^2 * kappa
    for (int i = 0; i < N; ++i) {
        if (i == 0) a_x[i] = (v_cmd[i+1] - v_cmd[i]) / dt;
        else if (i == N - 1) a_x[i] = (v_cmd[i] - v_cmd[i-1]) / dt;
        else a_x[i] = (v_cmd[i+1] - v_cmd[i-1]) / (2.0 * dt);

        a_y[i] = v_cmd[i] * v_cmd[i] * kappa[i];
    }

    double min_m_y = 1e9, min_m_x = 1e9, min_m_mu = 1e9;
    double min_v_safe = 1e9;

    for (int i = 0; i < N; ++i) {
        const double LTRy = ltr_y(a_y[i], p);
        const double LTRx = ltr_x(a_x[i], p);

        const double m_y = 1.0 - std::abs(LTRy);
        const double m_x = 1.0 - std::abs(LTRx);

        const double a_planar = std::sqrt(a_x[i]*a_x[i] + a_y[i]*a_y[i]);
        const double m_mu = p.mu * p.g - a_planar;

        min_m_y = std::min(min_m_y, m_y);
        min_m_x = std::min(min_m_x, m_x);
        min_m_mu = std::min(min_m_mu, m_mu);

        double v_tip, v_slide, v_safe;
        safe_speed_from_curvature(kappa[i], p, 3.0, v_tip, v_slide, v_safe);
        min_v_safe = std::min(min_v_safe, v_safe);
    }

    std::cout << std::fixed << std::setprecision(3);
    std::cout << "Worst (minimum) margins over trajectory:\\n";
    std::cout << "  min m_y  (roll tip): " << min_m_y << "\\n";
    std::cout << "  min m_x  (pitch tip): " << min_m_x << "\\n";
    std::cout << "  min m_mu (sliding): " << min_m_mu << "\\n";
    std::cout << "Worst speed limit (minimum v_safe): " << min_v_safe << "\\n";
    return 0;
}

7.3 Java

File: Chapter4_Lesson4.java

// Chapter4_Lesson4.java
// Stability and Tip-Over Risk (vehicle view) — reference implementation

import java.util.Locale;

public class Chapter4_Lesson4 {

    static class VehicleParams {
        public final double T;   // track width [m]
        public final double L;   // wheelbase [m]
        public final double h;   // CoG height [m]
        public final double mu;  // friction coefficient [-]
        public final double g;   // gravity [m/s^2]

        public VehicleParams(double T, double L, double h, double mu, double g) {
            this.T = T; this.L = L; this.h = h; this.mu = mu; this.g = g;
        }
    }

    static double ltrY(double aY, VehicleParams p) {
        return (2.0 * p.h * aY) / (p.g * p.T);
    }

    static double ltrX(double aX, VehicleParams p) {
        return (2.0 * p.h * aX) / (p.g * p.L);
    }

    static class SpeedBounds {
        public final double vTip;
        public final double vSlide;
        public final double vSafe;
        public SpeedBounds(double vTip, double vSlide, double vSafe) {
            this.vTip = vTip; this.vSlide = vSlide; this.vSafe = vSafe;
        }
    }

    static SpeedBounds safeSpeedFromCurvature(double kappa, VehicleParams p, double vCap) {
        double k = Math.abs(kappa);
        if (k < 1e-9) {
            return new SpeedBounds(vCap, vCap, vCap);
        }
        double vTip = Math.sqrt((p.g * p.T) / (2.0 * p.h * k));
        double vSlide = Math.sqrt((p.mu * p.g) / k);
        double vSafe = Math.min(vCap, Math.min(vTip, vSlide));
        return new SpeedBounds(vTip, vSlide, vSafe);
    }

    public static void main(String[] args) {
        Locale.setDefault(Locale.US);
        VehicleParams p = new VehicleParams(0.60, 0.80, 0.30, 0.70, 9.81);

        int N = 501;
        double t0 = 0.0, t1 = 10.0;
        double dt = (t1 - t0) / (N - 1);

        double[] t = new double[N];
        double[] kappa = new double[N];
        double[] vCmd = new double[N];
        double[] aX = new double[N];
        double[] aY = new double[N];

        for (int i = 0; i < N; i++) {
            t[i] = t0 + i * dt;
            kappa[i] = 0.4 * Math.sin(0.6 * t[i]);
            vCmd[i] = 1.8 + 0.6 * Math.sin(0.3 * t[i] + 0.7);
        }

        // aX finite difference; aY = v^2 * kappa
        for (int i = 0; i < N; i++) {
            if (i == 0) aX[i] = (vCmd[i + 1] - vCmd[i]) / dt;
            else if (i == N - 1) aX[i] = (vCmd[i] - vCmd[i - 1]) / dt;
            else aX[i] = (vCmd[i + 1] - vCmd[i - 1]) / (2.0 * dt);

            aY[i] = vCmd[i] * vCmd[i] * kappa[i];
        }

        double minMY = 1e9, minMX = 1e9, minMMu = 1e9, minVSafe = 1e9;

        for (int i = 0; i < N; i++) {
            double LTRy = ltrY(aY[i], p);
            double LTRx = ltrX(aX[i], p);

            double mY = 1.0 - Math.abs(LTRy);
            double mX = 1.0 - Math.abs(LTRx);

            double aPlanar = Math.sqrt(aX[i] * aX[i] + aY[i] * aY[i]);
            double mMu = p.mu * p.g - aPlanar;

            minMY = Math.min(minMY, mY);
            minMX = Math.min(minMX, mX);
            minMMu = Math.min(minMMu, mMu);

            SpeedBounds sb = safeSpeedFromCurvature(kappa[i], p, 3.0);
            minVSafe = Math.min(minVSafe, sb.vSafe);
        }

        System.out.printf("Worst (minimum) margins over trajectory:%n");
        System.out.printf("  min m_y  (roll tip): %.3f%n", minMY);
        System.out.printf("  min m_x  (pitch tip): %.3f%n", minMX);
        System.out.printf("  min m_mu (sliding): %.3f%n", minMMu);
        System.out.printf("Worst speed limit (minimum v_safe): %.3f%n", minVSafe);
    }
}

7.4 MATLAB / Simulink

File: Chapter4_Lesson4.m

% Chapter4_Lesson4.m
% Stability and Tip-Over Risk (vehicle view) — reference implementation
%
% This script computes:
%   - LTR_y = 2*h*a_y/(g*T), LTR_x = 2*h*a_x/(g*L)
%   - margins m_y = 1 - |LTR_y|, m_x = 1 - |LTR_x|
%   - friction margin m_mu = mu*g - sqrt(a_x^2 + a_y^2)
%   - curvature-based safe speeds: v_tip, v_slide, v_safe
%
% Notes for Simulink users:
%   Implement the same formulas inside a MATLAB Function block to create
%   an online stability monitor. The inputs would be v, kappa, and a_x.

clear; clc;

% Vehicle parameters
T = 0.60;   % track width [m]
L = 0.80;   % wheelbase [m]
h = 0.30;   % CoG height [m]
mu = 0.70;  % friction coefficient [-]
g = 9.81;   % gravity [m/s^2]

% Synthetic trajectory signals
N = 501;
t = linspace(0,10,N);
dt = t(2) - t(1);

kappa = 0.4 * sin(0.6 * t);             % [1/m]
v_cmd = 1.8 + 0.6 * sin(0.3 * t + 0.7); % [m/s]

% Accelerations
a_y = (v_cmd.^2) .* kappa;              % steady-turn approx
a_x = gradient(v_cmd, dt);

% LTRs
LTR_y = (2*h.*a_y) ./ (g*T);
LTR_x = (2*h.*a_x) ./ (g*L);

% Margins
m_y = 1 - abs(LTR_y);
m_x = 1 - abs(LTR_x);
a_planar = sqrt(a_x.^2 + a_y.^2);
m_mu = mu*g - a_planar;

% Safe speeds from curvature
v_cap = 3.0;
k = abs(kappa);
v_tip = sqrt((g*T) ./ (2*h*max(k, 1e-12)));
v_slide = sqrt((mu*g) ./ max(k, 1e-12));

v_tip(k < 1e-9) = v_cap;
v_slide(k < 1e-9) = v_cap;
v_safe = min(v_cap, min(v_tip, v_slide));

fprintf('Worst (minimum) margins over trajectory:\\n');
fprintf('  min m_y  (roll tip): %.3f\\n', min(m_y));
fprintf('  min m_x  (pitch tip): %.3f\\n', min(m_x));
fprintf('  min m_mu (sliding): %.3f\\n', min(m_mu));
fprintf('Worst speed limit (minimum v_safe): %.3f\\n', min(v_safe));

% Plots
figure; plot(t, v_cmd, t, v_safe); grid on;
xlabel('t [s]'); ylabel('speed [m/s]'); legend('v\\_cmd','v\\_safe');

figure; plot(t, LTR_y, t, LTR_x); grid on;
xlabel('t [s]'); ylabel('LTR [-]'); legend('LTR\\_y','LTR\\_x');

7.5 Wolfram Mathematica

File: Chapter4_Lesson4.nb

(* Chapter4_Lesson4.nb *)
Notebook[{
  Cell["Chapter 4 — Lesson 4: Stability and Tip-Over Risk (vehicle view)", "Title"],
  Cell["This notebook computes LTR metrics and curvature-based safe speed limits.", "Text"],
  Cell[BoxData[RowBox[{RowBox[{"ClearAll","[","\"Global`*\"","]"}],";"}]],"Input"],
  Cell["Vehicle parameters", "Section"],
  Cell[BoxData[
    RowBox[{
      RowBox[{"T","=","0.60"}],";",
      RowBox[{"L","=","0.80"}],";",
      RowBox[{"h","=","0.30"}],";",
      RowBox[{"mu","=","0.70"}],";",
      RowBox[{"g","=","9.81"}],";"
    }]],"Input"],
  Cell["Synthetic trajectory signals", "Section"],
  Cell[BoxData[
    RowBox[{
      RowBox[{"N","=","501"}],";",
      RowBox[{"t","=",RowBox[{"Subdivide","[",RowBox[{"0",",","10",",",RowBox[{"N","-","1"}]}],"]"}]}],";",
      RowBox[{"dt","=",RowBox[{RowBox[{"t","[",RowBox[{"[","2","]"}],"]"}],"-",RowBox[{"t","[",RowBox[{"[","1","]"}],"]"}]}]}],";",
      RowBox[{"kappa","=",RowBox[{"0.4"," ",RowBox[{"Sin","[",RowBox[{"0.6"," ","t"}],"]"}]}]}],";",
      RowBox[{"vCmd","=",RowBox[{"1.8","+",
        RowBox[{"0.6"," ",RowBox[{"Sin","[",RowBox[{"0.3"," ","t","+","0.7"}],"]"}]}]}]}],";"
    }]],"Input"],
  Cell["Accelerations (steady-turn approx + finite difference)", "Section"],
  Cell[BoxData[
    RowBox[{
      RowBox[{"aY","=",RowBox[{"vCmd"^"2"," ","kappa"}]}],";",
      RowBox[{"aX", "=", RowBox[{
        RowBox[{"ListCorrelate","[",RowBox[{RowBox[{"{",RowBox[{"-1",",","0",",","1"}],"}"}],",","vCmd",",","2"}],"]"}],
        "/",RowBox[{"(",RowBox[{"2"," ","dt"}],")"}]
      }]}],";",
      RowBox[{"aX","=",RowBox[{"Join","[",
        RowBox[{RowBox[{"{",RowBox[{"aX","[",RowBox[{"[","1","]"}],"]"}],"}"}],",","aX",",",RowBox[{"{",RowBox[{"aX","[",RowBox[{"[",RowBox[{"-1"}],"]"}],"]"}],"}"}]}],"]"}]}],";"
    }]],"Input"],
  Cell["LTR and margins", "Section"],
  Cell[BoxData[
    RowBox[{
      RowBox[{"LTRy","=",RowBox[{RowBox[{"(",RowBox[{"2"," ","h"," ","aY"}],")"}],"/",RowBox[{"(",RowBox[{"g"," ","T"}],")"}]}]}],";",
      RowBox[{"LTRx","=",RowBox[{RowBox[{"(",RowBox[{"2"," ","h"," ","aX"}],")"}],"/",RowBox[{"(",RowBox[{"g"," ","L"}],")"}]}]}],";",
      RowBox[{"mY","=",RowBox[{"1","-",RowBox[{"Abs","[","LTRy","]"}]}]}],";",
      RowBox[{"mX","=",RowBox[{"1","-",RowBox[{"Abs","[","LTRx","]"}]}]}],";",
      RowBox[{"mMu","=",RowBox[{"mu"," ","g","-",RowBox[{"Sqrt","[",RowBox[{"aX"^"2","+","aY"^"2"}],"]"}]}]}],";"
    }]],"Input"],
  Cell["Curvature-based safe speed bounds", "Section"],
  Cell[BoxData[
    RowBox[{
      RowBox[{"vCap","=","3.0"}],";",
      RowBox[{"k","=",RowBox[{"Abs","[","kappa","]"}]}],";",
      RowBox[{"vTip","=",RowBox[{"Sqrt","[",RowBox[{RowBox[{"(",RowBox[{"g"," ","T"}],")"}],"/",RowBox[{"(",RowBox[{"2"," ","h"," ",RowBox[{"Max","[",RowBox[{"k",",","10"^"-12"}],"]"}]}],")"}]}],"]"}]}],";",
      RowBox[{"vSlide","=",RowBox[{"Sqrt","[",RowBox[{RowBox[{"(",RowBox[{"mu"," ","g"}],")"}],"/",RowBox[{"Max","[",RowBox[{"k",",","10"^"-12"}],"]"}]}],"]"}]}],";",
      RowBox[{"vTip","=",RowBox[{"MapThread","[",RowBox[{"If","[",RowBox[{RowBox[#1,"<","10"^"-9"}],",","vCap",",","#2"}],"]"}],",",RowBox[{"{",RowBox[{"k",",","vTip"}],"}"}]}],"]"}]}],";",
      RowBox[{"vSlide","=",RowBox[{"MapThread","[",RowBox[{"If","[",RowBox[{RowBox[#1,"<","10"^"-9"}],",","vCap",",","#2"}],"]"}],",",RowBox[{"{",RowBox[{"k",",","vSlide"}],"}"}]}],"]"}]}],";",
      RowBox[{"vSafe","=",RowBox[{"MapThread","[",RowBox[{"Min","[",RowBox[{"#1",",","#2",",","vCap"}],"]"}],",",RowBox[{"{",RowBox[{"vTip",",","vSlide"}],"}"}]}],"]"}]}],";"
    }]],"Input"],
  Cell["Worst-case margins and plots", "Section"],
  Cell[BoxData[
    RowBox[{
      RowBox[{"Print","[",RowBox[{"\"min m_y (roll tip) = \"",",",RowBox[{"Min","[","mY","]"}]}],"]"}],";",
      RowBox[{"Print","[",RowBox[{"\"min m_x (pitch tip) = \"",",",RowBox[{"Min","[","mX","]"}]}],"]"}],";",
      RowBox[{"Print","[",RowBox[{"\"min m_mu (sliding) = \"",",",RowBox[{"Min","[","mMu","]"}]}],"]"}],";",
      RowBox[{"Print","[",RowBox[{"\"min v_safe = \"",",",RowBox[{"Min","[","vSafe","]"}]}],"]"}],";"
    }]],"Input"],
  Cell[BoxData[
    RowBox[{"ListLinePlot","[",
      RowBox[{
        RowBox[{"{",
          RowBox[{
            RowBox[{"Transpose","[",RowBox[{"{",RowBox[{"t",",","vCmd"}],"}"}],"]"}],",",
            RowBox[{"Transpose","[",RowBox[{"{",RowBox[{"t",",","vSafe"}],"}"}],"]"}]
          }],"}"}],",",
        RowBox[{"PlotLegends","->",RowBox[{"{",RowBox[{"\"v_cmd\"",",","\"v_safe\""}],"}"}]}],",",
        RowBox[{"GridLines","->","Automatic"}]
      }],"]"}]],"Input"],
  Cell[BoxData[
    RowBox[{"ListLinePlot","[",
      RowBox[{
        RowBox[{"{",
          RowBox[{
            RowBox[{"Transpose","[",RowBox[{"{",RowBox[{"t",",","LTRy"}],"}"}],"]"}],",",
            RowBox[{"Transpose","[",RowBox[{"{",RowBox[{"t",",","LTRx"}],"}"}],"]"}]
          }],"}"}],",",
        RowBox[{"PlotLegends","->",RowBox[{"{",RowBox[{"\"LTR_y\"",",","\"LTR_x\""}],"}"}]}],",",
        RowBox[{"GridLines","->","Automatic"}]
      }],"]"}]],"Input"]
},
WindowSize -> {1000, 700},
WindowMargins -> { {Automatic, 0}, {Automatic, 0} }
]

Robotics libraries to be aware of (ecosystem pointers): Python: NumPy/SciPy, ROS2 (rclpy), PyBullet; C++: Eigen, ROS2 (rclcpp); Java: rosjava (ROS 1 ecosystems) + EJML/JOML; MATLAB: Robotics System Toolbox + Simulink. The mathematical core in this lesson is independent of the stack.

8. Problems and Solutions

Problem 1 (moment-based tip threshold): A symmetric robot has track width \( T \), CoG height \( h \), and experiences lateral acceleration \( a_y \). Starting from a roll moment balance, derive the condition for inside-wheel lift-off.

Solution: With half-track \( b=T/2 \), quasi-static moment equilibrium gives \( (F_{zR}-F_{zL})b = m a_y h \) and \( F_{zR}+F_{zL}=mg \). Define \( \mathrm{LTR}_y=(F_{zR}-F_{zL})/(F_{zR}+F_{zL}) \), hence

\[ \mathrm{LTR}_y = \frac{2h}{gT}a_y. \]

Inside lift-off occurs when \( F_{zL}=0 \) (or \( F_{zR}=0 \) depending on turn direction), so \( |\mathrm{LTR}_y|=1 \). Therefore,

\[ |a_y| = g\,\frac{T}{2h}. \]


Problem 2 (speed limit on a planned curvature): A planner outputs a constant-curvature arc \( \kappa \) (units 1/m). The robot will execute it at constant speed \( v \). Derive the maximum speed ensuring no tip-over in the rigid quasi-static model.

Solution: For constant curvature, \( a_y=v^2\kappa \) (assuming sign handled separately). Enforce \( |a_y| \le g\,\dfrac{T}{2h} \):

\[ v^2|\kappa| \le g\,\frac{T}{2h} \;\Rightarrow\; v \le \sqrt{\frac{gT}{2h|\kappa|}}. \]


Problem 3 (slide-first vs tip-first decision): Assume steady turning so that sliding limit is \( |a_y| \le \mu g \). Show the inequality that determines whether tip-over happens before sliding.

Solution: Tip occurs first if its threshold is smaller:

\[ g\,\frac{T}{2h} \le \mu g \;\Rightarrow\; \frac{T}{2h} \le \mu. \]

If \( \dfrac{T}{2h} > \mu \), then friction saturates first and the robot is more likely to slide than to tip (under the simplified friction model).


Problem 4 (roll compliance steady-state amplification): Using the roll ODE \( I_\phi \ddot{\phi} + c_\phi \dot{\phi} + k_\phi \phi = m h a_y \), assume constant \( a_y \) and small angles. Derive \( \phi_{ss} \) and show how it amplifies \( \mathrm{LTR}_{y,ss} \) in the approximation \( \mathrm{LTR}_y \approx \dfrac{2h}{gT}a_y + \dfrac{2h}{T}\phi \).

Solution: For constant input, steady-state implies \( \dot{\phi}=\ddot{\phi}=0 \), so

\[ k_\phi \phi_{ss} = m h a_y \;\Rightarrow\; \phi_{ss} = \frac{m h}{k_\phi}a_y. \]

Substitute into the LTR approximation:

\[ \mathrm{LTR}_{y,ss} \approx \frac{2h}{gT}a_y + \frac{2h}{T}\left(\frac{m h}{k_\phi}a_y\right) = \frac{2h}{gT}a_y\left(1+\frac{m g h}{k_\phi}\right). \]

Thus, finite roll compliance increases steady-state load transfer for the same \( a_y \), reducing the effective safe lateral acceleration compared with the rigid-body bound.


Problem 5 (risk margins from data): Given time series of \( a_y(t) \), define \( m_y(t)=1-|\mathrm{LTR}_y(t)| \). For a robot with \( T=0.6 \) m and \( h=0.3 \) m, compute \( \mathrm{LTR}_y \) and \( m_y \) at \( a_y=3.0 \) m/s² and decide if it is safe (rigid quasi-static model).

Solution: Compute \( \mathrm{LTR}_y = \dfrac{2h}{gT}a_y \). With \( g=9.81 \), \( T=0.6 \), \( h=0.3 \):

\[ \mathrm{LTR}_y = \frac{2(0.3)}{9.81(0.6)}(3.0) = \frac{0.6}{5.886}(3.0) \approx 0.3057. \]

Then \( m_y = 1-|\mathrm{LTR}_y| \approx 0.6943 \), which is safely positive and far from the tip threshold \( |\mathrm{LTR}_y|=1 \).

9. Summary

We derived vehicle-centric tip-over conditions using (i) the support-polygon line-of-action criterion and (ii) the load-transfer (LTR) formulation that directly connects to measurable normal-load redistribution. We obtained closed-form bounds for lateral and longitudinal tipping, compared them to friction-limited sliding, and showed how a minimal roll compliance model can conservatively reduce stability margins. The resulting formulas are immediately implementable as an online safety monitor and as a curvature-based speed limiter.

10. References

  1. Piyabongkarn, D., Lew, J., Rajamani, R., Grogg, J., & Yuan, Q. (2009). On the Use of the Load Transfer Ratio for Vehicle Rollover Detection. Vehicle System Dynamics, 47(7), 901–918.
  2. Solmaz, S., Corless, M., & Shorten, R. (2007). A Methodology for the Design of Robust Rollover Prevention Controllers for Automotive Vehicles. International Journal of Control, 80(11), 1763–1779.
  3. Hac, A., & Youn, I. (1992). Rollover Stability Indices for Vehicle Dynamics Analysis. Vehicle System Dynamics, 21(S1), 335–346.
  4. Chen, B.-C., Peng, H., & Grizzle, J. (2001). Dynamic Rollover Prevention for Sport Utility Vehicles. IEEE Transactions on Control Systems Technology, 9(6), 849–863.
  5. Phanomchoeng, G., & Rajamani, R. (2013). New Rollover Index for the Detection and Prevention of Vehicle Rollover. IEEE Transactions on Intelligent Transportation Systems, 14(3), 1372–1381.
  6. Shim, T., & Velusamy, K. (2007). Rollover Prevention with Side Slip Angle Estimation and Active Steering. Vehicle System Dynamics, 45(7–8), 659–674.
  7. Andrieu, C., & Midha, A. (2014). Model-Based Evaluation of Tip-Over Risk for Mobile Robots with High Center of Mass. Robotics and Autonomous Systems, 62(12), 1788–1799.