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
- 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.
- 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.
- Hac, A., & Youn, I. (1992). Rollover Stability Indices for Vehicle Dynamics Analysis. Vehicle System Dynamics, 21(S1), 335–346.
- 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.
- 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.
- Shim, T., & Velusamy, K. (2007). Rollover Prevention with Side Slip Angle Estimation and Active Steering. Vehicle System Dynamics, 45(7–8), 659–674.
- 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.
Help keep these engineering tutorials free and growing
If these lessons, examples, and project pages help you, a small donation supports the continued creation and improvement of free control, robotics, software, and engineering education resources.
Created and maintained by Abolfazl Mohammadijoo.