From 88976fee1221321039b99cbe2c0611b0ee1b67e2 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 00:22:07 +0530 Subject: [PATCH 01/29] docs: added MPC doc --- doc/4_path_tracking/4_2_mpc_controller.md | 654 +++++++++++++++++++++ doc/4_path_tracking/4_3_mppi_controller.md | 619 +++++++++++++++++++ doc/4_path_tracking/mppi.png | Bin 0 -> 142125 bytes 3 files changed, 1273 insertions(+) create mode 100644 doc/4_path_tracking/4_2_mpc_controller.md create mode 100644 doc/4_path_tracking/4_3_mppi_controller.md create mode 100644 doc/4_path_tracking/mppi.png diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md new file mode 100644 index 0000000..b71bb71 --- /dev/null +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -0,0 +1,654 @@ +# 5. MPC Controller + +In this chapter, the MPC (Model Predictive Control) path tracking controller class is implemented. This class implements the MPC path tracking algorithm, which computes a steering angle and acceleration command by solving an **optimization problem** over a finite prediction horizon at every time step. + +Before getting into the code, let's get some basic understanding behind the algorithm: + +### **Model Predictive Control (MPC)** + +- Also known as **Receding Horizon Control**. +- Widely used in autonomous vehicles, robotics, and industrial process control. +- At every time step, MPC solves an Optimal Control Problem (OCP) over a future horizon of **N steps**, then applies only the **first control action** and repeats the process at the next step. + +**The key idea is**: instead of reacting to the current error (like Stanley or PID), MPC **predicts** where the vehicle will be over the next N steps and finds the control sequence that minimises a cost function while respecting the hard constraints. + +--- + +**Advantages over Stanley / Pure Pursuit / PID :** + +- Handles **hard constraints** natively - steering limits, acceleration limits, and speed bounds are enforced by the solver, not violated and then clipped(like in geometric controllers). +- Plans **N steps ahead** - can anticipate future curves before the vehicle reaches them and adjust the control input according to that. +- Combines tracking error, control effort, and smoothness of control inputs in one unified cost function. +- Natively handles variable speed profiles along the course. + +**Disadvantages:** + +- Computationally heavier : requires solving an NLP (Non-Linear Program) at every step via IPOPT. +- Requires tuning of multiple cost weights. +- Sensitive to model mismatch - if the internal vehicle model differs from the real vehicle, tracking performance degrades. + +--- + +#### Libraries used: + +1. **CasADi** is a symbolic math library. When you write $p_x + v * cos(\theta) * dt$ in CasADi, it does not compute a number, it stores the formula as an expression tree, like algebra. The reason this matters is that IPOPT needs the derivative of every equation at every iteration. CasADi computes those derivatives analytically and exactly (automatic differentiation), the same way we'd differentiate by hand. Without this, we'd have to approximate derivatives numerically, which is slow and inaccurate. In the MPC code, every `model.set_rhs()` call is for handing CasADi a symbolic expression to work with. + +2. **do-mpc** is the MPC framework that sits on top of CasADi. It handles all the boilerplate - assembling the horizon loop, managing the decision variable layout, calling the `tvp_fun()` before each solve to inject the latest reference trajectory, passing everything to *IPOPT*, reading the solution, and shifting the warm start for next time. Without do-mpc we'd write all of that by hand. With do-mpc, `make_step(x0)` does all of it in one call. + +--- + +#### Important terms: + +1. **IPOPT** (Interior Point OPTimizer) is the actual numerical solver - the thing that finds the answer. It receives a cost value and its gradient, constraint violations and their Jacobians, from CasADi at each iteration. It then takes a step using the interior point method (imagine a ball rolling down a valley while staying inside a fence), and repeats until the gradient is near zero ($\epsilon$) and all constraints are satisfied. It returns the optimal [steer, accel] sequence. + +2. A Nonlinear Program (NLP) is the class of math problem MPC belongs to. It is *"nonlinear"* because the bicycle model contains tan(steer) and cos(yaw) curved functions. That makes it harder than a linear problem (which has exactly one minimum you can jump to directly). NLPs can have multiple local minima, which is why warm-starting from the previous solution matters so much, and why **MPPI** (which samples globally) can sometimes find better solutions than MPC on tricky(back to back tight curves) courses. + +--- + +## 5.1 MpcController Class + +The controller class is located at: +[mpc_controller.py](/src/components/control/mpc/mpc_controller.py) + +```python +""" +mpc_controller.py +""" + +import math +import time +import warnings +import numpy as np +import do_mpc +import casadi as ca + + +class MpcController: + """ + do-mpc-based MPC path-tracking controller. + Public interface identical to MppiController. + """ +``` + +This class uses **do-mpc** as the MPC framework and **CasADi** as the symbolic algebra backend. CasADi automatically computes the exact gradients and Hessians that the IPOPT solver needs and so, no finite-difference approximations are required. + +--- + +### 5.1.1 Constructor + +```python +def __init__(self, spec, course=None, color="r", + delta_t=0.05, horizon_step_T=15, + stage_cost_weight=None, + terminal_cost_weight=None, + control_cost_weight=None, + smoothness_cost_weight=None, + max_steer_abs=0.523, + max_accel_abs=2.0, + v_min=-1.0, v_max=20.0, + ipopt_max_iter=80, + ipopt_print_level=0, + visualize_optimal_traj=True): + + self.WHEEL_BASE_M = spec.wheel_base_m + self.course = course + self.delta_t = delta_t + self.T = horizon_step_T + + self._sw = np.asarray(stage_cost_weight or [50., 50., 1., 20.]) + self._tw = np.asarray(terminal_cost_weight or self._sw * 2.) + self._cw = np.asarray(control_cost_weight or [1.0, 0.5]) + self._smw = np.asarray(smoothness_cost_weight or [5.0, 2.0]) + + self.max_steer = max_steer_abs + self.max_accel = max_accel_abs + self.v_min = v_min + self.v_max = v_max + + self.target_accel_mps2 = 0.0 + self.target_steer_rad = 0.0 + self.target_yaw_rate_rps = 0.0 + self._prev_waypoint_idx = 0 + + self._build_arclength_table() + self._model = self._build_model() + self._mpc = self._build_mpc(self._model) +``` + +The constructor takes a `VehicleSpecification` object and an optional `CubicSplineCourse` object. The key member variables are: + +| Variable | Default | Description | +|---|---|---| +| `WHEEL_BASE_M` | from spec | Distance between front and rear axles [m] | +| `T` | 15 | Prediction horizon - number of steps looked ahead | +| `delta_t` | 0.05 | Time step for each horizon step [s] | +| `_sw` | [50, 50, 1, 20] | Stage cost weights `[w_x, w_y, w_ψ, w_v]` | +| `_tw` | 2 × `_sw` | Terminal cost weights — heavier than stage | +| `_cw` | [1.0, 0.5] | Control effort weights `[w_δ, w_a]` | +| `_smw` | [5.0, 2.0] | Smoothness weights `[w_Δδ, w_Δa]` | +| `max_steer` | 0.523 rad | Hard steering bound (~30°) | +| `max_accel` | 2.0 m/s² | Hard acceleration bound | +| `v_min / v_max` | -1 / 20 m/s | Hard speed bounds | +| `target_accel_mps2` | 0.0 | Computed acceleration command [m/s²] | +| `target_steer_rad` | 0.0 | Computed steering angle command [rad] | +| `target_yaw_rate_rps` | 0.0 | Computed yaw rate command [rad/s] | + +--- + +## 5.2 Algorithm Background + +### 5.2.1 State and Control Vectors + +MPC operates on a **state vector** $x$ and a **control vector** $u$: + +$$ +x_t = +\begin{bmatrix} +p_x & p_y & \psi & v +\end{bmatrix}^{T} +$$ + +$$ +u_t = +\begin{bmatrix} +\delta & a +\end{bmatrix}^{T} +$$ + + + +At each time step, MPC finds the sequence $U = {u₀, u₁, …, u_{N-1}}$ that minimises the total cost $J$ over the horizon. + +--- + +### 5.2.2 Vehicle Model - Discrete Bicycle Kinematics + +MPC uses an internal motion model to predict how the vehicle will move in response to each control. The **kinematic bicycle model** is used: + +$$ +\begin{aligned} +p_{x,t+1} &= p_{x,t} + v_t \cos(\psi_t)\,\Delta t, \\ +p_{y,t+1} &= p_{y,t} + v_t \sin(\psi_t)\,\Delta t, \\ +\psi_{t+1} &= \psi_t + \frac{v_t}{L}\tan(\delta_t)\,\Delta t, \\ +v_{t+1} &= v_t + a_t\,\Delta t. +\end{aligned} +$$ + +where $L$ is the wheelbase and $Δt$ is the time step. This is the same kinematic relationship used in the other controller also, only the key difference is that MPC applies it **N times symbolically** to predict the full future trajectory. + +CasADi writes these equations as symbolic expressions, meaning it can automatically compute exact derivatives, the same way you would differentiate by hand, but done by the computer. + +--- + +### 5.2.3 The Optimization Problem + +At every time step, MPC solves the optimization problem: + +$$ +\begin{aligned} +\min_{U}\quad +J +&= +\sum_{t=0}^{N-1} +\ell(x_t,u_t,u_{t-1}) ++ +\phi(x_N) +\\[6pt] +\text{subject to}\quad +& +x_{t+1} += +f(x_t,u_t), +\qquad t=0,\ldots,N-1 +\\[4pt] +& +|\delta_t| +\le +\delta_{\max} +\\[4pt] +& +|a_t| +\le +a_{\max} +\\[4pt] +& +v_{\min} +\le +v_t +\le +v_{\max} +\end{aligned} +$$ + +where: + +- $x_{t+1}=f(x_t,u_t)$ enforces the bicycle model dynamics at every prediction step. +- $|\delta_t| \le \delta_{\max}$ imposes a hard steering constraint. +- $|a_t| \le a_{\max}$ imposes a hard acceleration constraint. +- $v_{\min} \le v_t \le v_{\max}$ imposes hard speed limits. + +The constraints are **hard constraints**, meaning the optimizer (e.g., IPOPT) enforces them directly during optimization rather than clipping the control output afterward. + +--- + +### 5.2.4 Stage Cost $\ell(x_t, u_t, u_{t-1})$ + +The stage cost is evaluated at every step $t = 0,\ldots,N-1$ of the horizon. It has three parts: + +#### 1. Tracking Error + +Penalises deviation from the reference trajectory: + +$$ +\ell_{\text{track}} += +w_x (p_{x,t} - p_{x,r})^2 ++ +w_y (p_{y,t} - p_{y,r})^2 ++ +w_\psi +\left[ +\operatorname{atan2} +\left( +\sin(\psi_t-\psi_r), +\cos(\psi_t-\psi_r) +\right) +\right]^2 ++ +w_v (v_t-v_r)^2 +$$ + +The heading error uses + +$$ +\operatorname{atan2} +\left( +\sin(\psi_t-\psi_r), +\cos(\psi_t-\psi_r) +\right) +$$ + +rather than a direct subtraction. This maps the error into: + +$$ +(-\pi,\pi] +$$ + +and remains smooth when the angle wraps around $\pm\pi$. + +--- + +#### 2. Control Effort + +Penalises large control inputs: + +$$ +\ell_{\text{effort}} += +w_\delta \,\delta_t^2 ++ +w_a\,a_t^2 +$$ + +--- + +#### 3. Smoothness + +Penalises rapid changes between consecutive control inputs: + +$$ +\ell_{\text{smooth}} += +w_{\Delta\delta} +(\delta_t-\delta_{t-1})^2 ++ +w_{\Delta a} +(a_t-a_{t-1})^2 +$$ + +This is implemented via do-mpc's `set_rterm()` which automatically adds this term at every horizon step. It prevents the controller from producing jerky steering commands. + +In code, effort and smoothness are combined into a single `rterm` weight per channel: + +```python +mpc.set_rterm( + steer = control_cost_weight[0] + smoothness_cost_weight[0], + accel = control_cost_weight[1] + smoothness_cost_weight[1], +) +``` + +--- + +### 5.2.5 Terminal Cost $\phi(x_N)$ + +The terminal cost is evaluated only at the **last step** $t = N$ of the horizon: + +$$ +\phi(x_N) += +W_x (p_{x,N} - p_{x,rN})^2 ++ +W_y (p_{y,N} - p_{y,rN})^2 ++ +W_\psi +\left[ +\operatorname{atan2} +\left( +\sin(\psi_N-\psi_{rN}), +\cos(\psi_N-\psi_{rN}) +\right) +\right]^2 ++ +W_v (v_N-v_{rN})^2 +$$ + +The terminal weights $W$ are set heavier than the stage weights (default: $W = 2 × w$). Without a terminal cost, the optimizer has no incentive to reach a good state at the end of the horizon, it can "borrow" performance from future steps that it cannot see. + +--- + +### 5.2.6 The Receding Horizon Principle + +MPC solves for the full sequence ${u₀, u₁, …, u_{N-1}}$ but **only applies $u₀$** - the first control action. At the next time step, the horizon shifts forward by one step, the current state is re-measured, and the problem is solved again from scratch. This is why it is called a **receding horizon controller**. + +At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: + +$$ +U^* += +\left\{ +u_0^*,\,u_1^*,\,\ldots,\,u_{N-1}^* +\right\} +$$ + +but only + +$$ +u(k)=u_0^* +$$ + +is applied to the vehicle. + +At the next sampling instant, the optimization is solved again using the updated state estimate: + +$$ +x(k+1) +$$ + +resulting in a new optimal control sequence. + +This strategy is known as the **receding horizon** (or **moving horizon**) principle. + +**Warm starting**: the previous solution is shifted by one step and used as the initial guess for the next solve. This means *IPOPT* starts close to the true optimum and converges in far fewer iterations. + +--- + +## 5.3 Private Methods + +### 5.3.1 `_build_arclength_table()` + +```python +def _build_arclength_table(self): + xs, ys, yaws, speeds = [], [], [], [] + n = 0 + while True: + try: + xs.append(self.course.point_x_m(n)) + ... + n += 1 + except Exception: + break + + seg = np.hypot(np.diff(xs), np.diff(ys)) + arc = np.concatenate([[0.0], np.cumsum(seg)]) + total = arc[-1] + + self._ext_x = np.tile(xs, 3) + self._ext_arc = np.concatenate([arc, arc + total, arc + 2 * total]) +``` + +This method is called once in `__init__()`. It samples every course point, computes the **cumulative arc-length** (total distance along the curve from the start), and tiles the arrays three times. + +The tiling handles closed or looping courses: when a vehicle near the end of the course needs to look $T$ steps ahead, the indices wrap around to the beginning. Without tiling, you would need modular arithmetic at every reference lookup; with tiling, the arrays are simply longer and you index linearly. + +--- + +### 5.3.2 `_build_model()` + +```python +def _build_model(self): + model = do_mpc.model.Model("discrete") + + model.set_variable("_x", "px") + model.set_variable("_x", "py") + model.set_variable("_x", "yaw") + model.set_variable("_x", "vel") + model.set_variable("_u", "steer") + model.set_variable("_u", "accel") + model.set_variable("_tvp", "ref_x") + model.set_variable("_tvp", "ref_y") + model.set_variable("_tvp", "ref_yaw") + model.set_variable("_tvp", "ref_vel") + + yaw_err = ca.atan2( + ca.sin(model.x["yaw"] - model.tvp["ref_yaw"]), + ca.cos(model.x["yaw"] - model.tvp["ref_yaw"]), + ) + model.set_expression("yaw_err", yaw_err) + + model.set_rhs("px", model.x["px"] + model.x["vel"] * ca.cos(model.x["yaw"]) * dt) + model.set_rhs("py", model.x["py"] + model.x["vel"] * ca.sin(model.x["yaw"]) * dt) + model.set_rhs("yaw", model.x["yaw"] + model.x["vel"] / L * ca.tan(model.u["steer"]) * dt) + model.set_rhs("vel", model.x["vel"] + model.u["accel"] * dt) + + model.setup() + return model +``` + +This method declares the **symbolic bicycle model** using do-mpc's `Model` class. Every variable registered here becomes a CasADi symbolic expression - the same algebra that a mathematician would write, but executable and automatically differentiable. + +Three types of symbolic variables are registered: + +| Variable type | Names | Description | +|---|---|---| +| `_x` (state) | `px, py, yaw, vel` | The four vehicle states | +| `_u` (control) | `steer, accel` | The two control inputs | +| `_tvp` (time-varying) | `ref_x, ref_y, ref_yaw, ref_vel` | Reference trajectory changes each step | + +The **time-varying parameter (TVP)** is the key do-mpc feature that allows the reference trajectory to be updated every step. do-mpc calls `tvp_fun()` automatically before each solve and fills in the reference values for all `N+1` horizon steps. + +The `yaw_err` auxiliary expression is registered separately so it can be referenced cleanly in the cost function. It uses `atan2(sin(·), cos(·))` for the same reason as in the cost formulation above. + +--- + +### 5.3.3 `_build_mpc(model)` + +```python +def _build_mpc(self, model): + mpc = do_mpc.controller.MPC(model) + mpc.set_param(n_horizon=self.T, t_step=self.delta_t, + n_robust=0, store_full_solution=True, + nlpsol_opts=self._ipopt_opts) + + # TVP function — closure over self._current_ref + tvp_template = mpc.get_tvp_template() + def tvp_fun(t_now): + for k in range(self.T + 1): + tvp_template["_tvp", k, "ref_x"] = float(self._current_ref[0, k]) + tvp_template["_tvp", k, "ref_y"] = float(self._current_ref[1, k]) + tvp_template["_tvp", k, "ref_yaw"] = float(self._current_ref[2, k]) + tvp_template["_tvp", k, "ref_vel"] = float(self._current_ref[3, k]) + return tvp_template + mpc.set_tvp_fun(tvp_fun) + + lterm = (sw[0]*(x["px"]-tvp["ref_x"])**2 + sw[1]*(x["py"]-tvp["ref_y"])**2 + + sw[2]*model.aux["yaw_err"]**2 + sw[3]*(x["vel"]-tvp["ref_vel"])**2) + mterm = (tw[0]*(x["px"]-tvp["ref_x"])**2 + ...) + + mpc.set_objective(lterm=lterm, mterm=mterm) + mpc.set_rterm(steer=cw[0]+smw[0], accel=cw[1]+smw[1]) + + mpc.bounds["lower", "_u", "steer"] = -self.max_steer + mpc.bounds["upper", "_u", "steer"] = self.max_steer + mpc.bounds["lower", "_u", "accel"] = -self.max_accel + mpc.bounds["upper", "_u", "accel"] = self.max_accel + mpc.bounds["lower", "_x", "vel"] = self.v_min + mpc.bounds["upper", "_x", "vel"] = self.v_max + + mpc.setup() + return mpc +``` + +This method assembles the full MPC optimization problem using the do-mpc API: + +- `set_param()` - sets the horizon length, time step, and IPOPT options. +- `set_tvp_fun()` - registers the TVP closure so do-mpc calls it automatically before each solve. +- `set_objective(lterm, mterm)` - symbolic stage and terminal costs. `lterm` is evaluated at every step $t = 0…N-1$; `mterm` only at step $t = N$. +- `set_rterm()` - registers the smoothness/effort Δu penalty. do-mpc adds $r_i·(u_{i,t} − u_{i,t−1})²$ at every horizon step automatically. +- `bounds[]` - hard box constraints on steering, acceleration, and speed. + +--- + +### 5.3.4 `_get_reference_trajectory(x0, y0, current_speed)` + +```python +def _get_reference_trajectory(self, x0, y0, current_speed): + v_ref = max(abs(current_speed), 1.0) + step_len = v_ref * self.delta_t # arc-length between ref points + + # Forward search: find nearest index ahead of previous position + WINDOW = 60 + best_idx = self._prev_waypoint_idx + for off in range(WINDOW): + ci = self._prev_waypoint_idx + off + dist = (self._ext_x[ci] - x0)**2 + (self._ext_y[ci] - y0)**2 + if dist < best_dist: + best_idx = ci + self._prev_waypoint_idx = best_idx + + # Arc-length spaced reference: ref[k] is at distance k × v × Δt ahead + s0 = self._ext_arc[best_idx] + for k in range(self.T + 1): + target_s = s0 + k * step_len + ki = argmin |ext_arc[best_idx:] - target_s| + ref[:, k] = ext_x[ki], ext_y[ki], ext_yaw[ki], ext_spd[ki] + return ref +``` + +This method builds the $(4, T+1)$ reference array that is injected into the NLP via the TVP function. It has two important design decisions: + +**Forward-only search**: the nearest index is found by searching only *ahead* of `_prev_waypoint_idx`, never behind it. This prevents jumping to the wrong arc on a closed or figure-8 course. + +**Arc-length spaced reference**: reference point $k$ is placed at arc-length distance $k × v × Δt$ along the course ahead of the vehicle. This ensures the reference horizon always spans exactly the distance the vehicle will travel in $N$ steps - no more, no less. Without this, the reference would "run ahead" of the vehicle on tight curves and produce a horizon that points in the wrong direction. + +The formula for each reference step is: + +``` +target_s = s₀ + k × v × Δt +ref[k] = course point nearest to arc-length target_s +``` + +where $s₀$ is the cumulative arc-length at the vehicle's current nearest point, and $v × Δt$ is the distance travelled per prediction step. + +--- + +## 5.4 Public Methods + +### 5.4.1 `update` + +```python +def update(self, state, time_s): + if not self.course: return + + px = float(state.get_x_m()) + py = float(state.get_y_m()) + yaw = float(state.get_yaw_rad()) + vel = float(state.get_speed_mps()) + x0 = np.array([[px], [py], [yaw], [vel]]) # (4, 1) column vector + + self._current_ref = self._get_reference_trajectory(px, py, vel) + + if not self._initialized: + self._mpc.x0 = x0 + self._mpc.set_initial_guess() + self._initialized = True + + u0 = self._mpc.make_step(x0) # returns [[steer], [accel]] + + steer0 = float(np.clip(np.asarray(u0).flat[0], -self.max_steer, self.max_steer)) + accel0 = float(np.clip(np.asarray(u0).flat[1], -self.max_accel, self.max_accel)) + + self.target_steer_rad = steer0 + self.target_accel_mps2 = accel0 + self.target_yaw_rate_rps = vel / self.WHEEL_BASE_M * tan(steer0) +``` + +This is the main entry point called every simulation frame by `FourWheelsVehicle`. It orchestrates all private methods in order: + +``` +update() + 1. Extract px, py, yaw, vel from state getters + 2. Build x0 as (4,1) column vector — do-mpc's required shape + 3. Build reference trajectory via _get_reference_trajectory() + 4. First call only: set initial guess so IPOPT starts feasibly + 5. mpc.make_step(x0) — do-mpc calls tvp_fun(), then solves the NLP + 6. Extract steer0, accel0 from solution u0 + 7. Compute yaw_rate = v / L * tan(steer0) (bicycle model formula) + 8. Extract predicted trajectory from opt_x_num for draw() +``` + +If no course is set, the method returns early without computing anything. + +**A note on $u_0$ extraction**: do-mpc returns $u_0$ as a $(2, 1)$ column vector, but the shape can vary depending on the version. Using `.flat[0]` and `.flat[1]` (rather than `[0,0]` and `[1,0]`) is robust to both `(2,1)` and `(2,)` shapes. + +The yaw rate is then derived from the steering angle using the kinematic bicycle model. + +$$ +\omega = v * tan(δ) / L +$$ + +--- + +### 5.4.2 Getter Methods + +```python +def get_target_accel_mps2(self): + return self.target_accel_mps2 + +def get_target_yaw_rate_rps(self): + return self.target_yaw_rate_rps + +def get_target_steer_rad(self): + return self.target_steer_rad +``` + +These three getter methods expose the computed control outputs. They are called by `FourWheelsVehicle` after each `update()` to apply the commands to the vehicle's state. The interface is identical to `StanleyController`. + +--- + +### 5.4.3 `draw` + +```python +def draw(self, axes, elems): + if self.visualize_optimal_traj and self.optimal_trajectory: + x_list, y_list = self.optimal_trajectory + (line,) = axes.plot(x_list, y_list, + color=self.DRAW_COLOR, + linewidth=2.0, + linestyle="--", + alpha=0.9, + label="MPC trajectory") + elems.append(line) +``` + +MPC controller draws the **predicted trajectory** - the optimal state sequence ${x₀*, x₁*, …, x_N*}$ from the last NLP solution. This red dashed line shows exactly where the MPC controller plans the vehicle to go over the next $N × Δt$ seconds. It is a direct visualisation of what the optimizer computed, which makes debugging tuning much easier. + +The trajectory is extracted from do-mpc's `opt_x_num` structure by name: + +```python +px_pred = [opt_x_num["_x", k, 0, "px"] for k in range(T + 1)] +py_pred = [opt_x_num["_x", k, 0, "py"] for k in range(T + 1)] +``` + +--- + +**Author**: Mohit Kumar diff --git a/doc/4_path_tracking/4_3_mppi_controller.md b/doc/4_path_tracking/4_3_mppi_controller.md new file mode 100644 index 0000000..4faf8be --- /dev/null +++ b/doc/4_path_tracking/4_3_mppi_controller.md @@ -0,0 +1,619 @@ +# 6. MPPI Controller + +In this chapter, the MPPI (Model Predictive Path Integral) path tracking controller class is implemented. This class implements the MPPI path tracking algorithm, which computes a steering angle and acceleration command by **sampling thousands of random control sequences**, rolling each one forward through the vehicle dynamics, scoring them by cost, and computing a weighted average update. + +Before getting into the code, let's get some basic understanding behind the algorithm: + +### **Model Predictive Path Integral (MPPI)** + +- Introduced by Williams et al. (2017) at Georgia Tech. +- Belongs to the family of **stochastic optimal control** methods. +- Instead of solving an optimization problem algebraically (like MPC), MPPI samples K random perturbations around a nominal control sequence, simulates each one, and combines them using **information-theoretic weighting**. + +The key idea is: run $K$ imagined futures in parallel, score each by how well it tracks the path, then take a weighted combination - low-cost futures get high weight, high-cost futures get low weight. + +1. **Sampling** - draw K random noise sequences from a Gaussian distribution +2. **Rollout** - simulate the vehicle forward T steps for each sample +3. **Weighting** - assign higher weight to samples with lower trajectory cost +4. **Update** - shift the nominal control sequence toward the best samples + +

+ MPPI overview +

+ +Ref: https://dilithjay.com/blog/mppi + +**Advantages over MPC:** + +- No solver required - the update is a simple weighted sum. +- Naturally parallelisable over K - runs efficiently on a GPU. +- Handles **non-convex, non-smooth** cost landscapes where gradient-based solvers struggle. +- Global exploration through stochastic sampling - can escape local minima. + +**Disadvantages:** + +- Constraints are **soft only** - satisfied by increasing cost, never hard-blocked(like MPC where hard constraints are inforced). +- Solution quality depends on K - more samples give a better approximation but cost more compute. +- Tuning parameters($λ$, $K$, $σ$) requires experimentation with no systematic design method. +- Can be noisy at low K values, producing jittery control. + +--- + +## 6.1 MppiController Class + +The controller class is located at: +[mppi_controller.py](/src/components/control/mppi/mppi_controller.py) + +```python +""" +mppi_controller.py + +Author: Shisato Yano +""" + +import math, sys +from pathlib import Path +from math import atan2, cos, sin, tan +import numpy as np + +from state import State + + +class MppiController: + """ + MPPI path-tracking controller aligned with standard formulation: + warm start (u_prev), exploitation/exploration sampling, stage + terminal cost, + control cost term (param_gamma * u.T @ inv(Sigma) @ v), and optional smoothing. + """ +``` + +This class imports trigonometric functions from Python's `math` module and `numpy` for vectorised sampling and matrix operations. It also imports the `State` class to use its `motion_model` static method for simulating the vehicle forward during each sample rollout. + +--- + +### 6.1.1 Constructor + +```python +def __init__(self, spec, course=None, color="g", + delta_t=0.05, horizon_step_T=20, + number_of_samples_K=256, + param_exploration=0.0, + param_lambda=50.0, param_alpha=1.0, + sigma_steer=0.1, sigma_accel=0.5, + max_steer_abs=0.523, max_accel_abs=2.0, + stage_cost_weight=None, + terminal_cost_weight=None, + moving_average_window=0, + visualize_optimal_traj=True, + visualize_sampled_trajs=True): + + self.WHEEL_BASE_M = spec.wheel_base_m + self.T = horizon_step_T + self.K = number_of_samples_K + self.param_lambda = max(1e-6, param_lambda) + self.param_gamma = param_lambda * (1.0 - param_alpha) + self.Sigma = np.array([[sigma_steer**2, 0.0], + [0.0, sigma_accel**2]]) + self.stage_cost_weight = np.asarray(stage_cost_weight + or [50.0, 50.0, 1.0, 20.0]) + self.terminal_cost_weight = np.asarray(terminal_cost_weight + or self.stage_cost_weight.copy()) + self.u_prev = np.zeros((self.T, 2)) +``` + +The constructor takes a `VehicleSpecification` object and an optional `CubicSplineCourse`. The key member variables are: + +| Variable | Default | Description | +|---|---|---| +| `WHEEL_BASE_M` | from spec | Distance between front and rear axles [m] | +| `T` | 20 | Prediction horizon - number of steps rolled out per sample | +| `K` | 256 | Number of sample trajectories drawn each step | +| `param_lambda` | 50.0 | Temperature $λ$ - controls sharpness of weighting | +| `param_gamma` | $λ(1−α)$ | Control cost scaling; $α=1 → γ=0$ (no control cost) | +| `Sigma` | diag$(σ_δ², σ_a²)$ | Noise covariance matrix for sampling | +| `stage_cost_weight` | [50, 50, 1, 20] | $[w_x, w_y, w_ψ, w_v]$ - stage tracking weights | +| `terminal_cost_weight` | same as stage | $[w_x, w_y, w_ψ, w_v]$ - terminal tracking weights | +| `u_prev` | zeros (T, 2) | Warm-start control sequence `[steer, accel]` per step | +| `target_accel_mps2` | 0.0 | Computed acceleration command [m/s²] | +| `target_steer_rad` | 0.0 | Computed steering angle command [rad] | +| `target_yaw_rate_rps` | 0.0 | Computed yaw rate command [rad/s] | + +--- + +## 6.2 Algorithm Background + +### 6.2.1 State and Control Vectors + +MPPI operates on a state vector $x_t$ and control vector $u_t$: + +$$ +x_t += +\begin{bmatrix} +x \\ +y \\ +\psi \\ +v +\end{bmatrix} +\qquad +\text{(position [m], heading [rad], speed [m/s])} +$$ + +$$ +u_t += +\begin{bmatrix} +\delta \\ +a +\end{bmatrix} +\qquad +\text{(steering angle [rad], acceleration [m/s2])} +$$ + +The **nominal control sequence** (warm-started from the previous control cycle) is stored as: + +$$ +U += +\left\{ +u_{\text{prev}}[0], +u_{\text{prev}}[1], +\ldots, +u_{\text{prev}}[T-1] +\right\} +$$ + +with shape: + +$$ +(T,\,2) +$$ + +--- + +### 6.2.2 Theoretical Foundation - Free Energy and KL Divergence + +MPPI is derived from the principle of minimising a **free energy** objective. The theoretical result shows that the optimal control update under a Gaussian prior is equivalent to minimising: + +$$ +J = E_Q[S(τ)] + λ · D_KL(Q ‖ P₀) +$$ + +where +- $S(τ)$ is the trajectory cost +- $Q$ is the sampling distribution +-$P₀$ is the uncontrolled prior +- $D_KL$ is the KL divergence measuring how far $Q$ departs from $P_0$. + +The closed-form solution to this gives the information-theoretic weight for each sample: +$$ +w_k ∝ exp( −S(k) / λ ) +$$ + +This is exactly the **softmin formula**: lower-cost samples receive exponentially higher weight. The temperature parameter $λ$ controls the sharpness: + + +- $λ → 0$ : only the single best sample contributes (greedy / deterministic) +- $λ → ∞$ : all samples receive equal weight (fully random) + + +--- + +### 6.2.3 Sampling - Exploitation and Exploration + +At each step, K noise sequences are drawn from a zero-mean Gaussian: + +``` +ε_{k,t} ~ N(0, Σ) for k = 1…K, t = 0…T-1 + +Σ = diag(σ_steer², σ_accel²) (diagonal — steer and accel noise are independent) +``` + +The K samples are split into two groups: + +``` +Exploitation samples (first (1 − param_exploration) × K): + v_{k,t} = clip( u_prev[t] + ε_{k,t} ) ← perturb warm start + +Exploration samples (last param_exploration × K): + v_{k,t} = clip( ε_{k,t} ) ← pure random, ignores warm start +``` + +The exploitation samples stay close to the previous solution and refine it. The exploration samples venture further afield and can discover better solutions when the warm start has drifted off-course. The `param_exploration` parameter controls the ratio. + +--- + +### 6.2.4 Trajectory Cost S(k) + +Each sample trajectory `k` accumulates cost across all T steps plus a terminal cost: + +``` +S(k) = Σ_{t=0}^{T-1} [ c(x_t^k) + γ · u_prev[t]ᵀ Σ⁻¹ v_{k,t} ] + ϕ(x_T^k) +``` + +**Stage tracking cost** `c(x)` — deviation from the reference path: + +``` +c(x) = w_x · (x − x_r)² + + w_y · (y − y_r)² + + w_ψ · atan2(sin(ψ − ψ_r), cos(ψ − ψ_r))² + + w_v · (v − v_r)² +``` + +The heading error uses `atan2(sin(·), cos(·))` for the same reason as MPC — it wraps the angle difference into `(−π, π]` and avoids discontinuities near ±π. + +**Control cost term** `γ · u_prev[t]ᵀ Σ⁻¹ v_{k,t}` — this term is the mathematical signature of the MPPI formulation. It penalises samples that deviate far from the warm-start control `u_prev`. The parameter `γ = λ(1 − α)` controls its strength: + +``` +param_alpha = 1.0 → γ = 0 (control cost off — pure tracking) +param_alpha = 0.0 → γ = λ (full control cost — conservative) +``` + +In practice `param_alpha = 0.98` or `1.0` works well for path tracking. + +**Terminal cost** `ϕ(x_T)` — same structure as `c(x)` but evaluated only at the last step of each rollout, using `terminal_cost_weight` instead of `stage_cost_weight`. + +--- + +### 6.2.5 Information-Theoretic Weighting + +Once all K trajectory costs are computed, the weights are calculated in three steps: + +**Step 1** — Subtract the minimum cost for numerical stability (prevents `exp` overflow): + +``` +ρ = min_k S(k) +``` + +**Step 2** — Compute unnormalised softmin weights: + +``` +η̃_k = exp( −(S(k) − ρ) / λ ) +``` + +**Step 3** — Normalise so weights sum to 1: + +``` +η = Σ_k η̃_k +w_k = η̃_k / η +``` + +The result is a proper probability distribution over the K samples. Samples with cost close to `ρ` (the best sample) get weight near `1/K` or higher; samples with cost far above `ρ` get weight near 0. + +--- + +### 6.2.6 Control Update + +The weighted perturbation sum is computed across all K samples for each horizon step: + +``` +w_ε[t] = Σ_k w_k · ε_{k,t} shape: (T, 2) +``` + +This is the information-theoretically optimal update direction — a weighted combination of all noise perturbations, pulled toward the samples that worked best. + +**Optional smoothing** — a moving-average filter can be applied to `w_ε` along the time axis: + +``` +w_ε ← MovingAverage(w_ε, window) +``` + +This reduces chatter in the control sequence at the cost of slightly slower response. + +**Final update and warm-start shift:** + +``` +u = clip( u_prev + w_ε ) ← updated control sequence + +apply u[0] → steer0, accel0 ← first action executed this step + +warm-start shift for next step: + u_prev[0..T-2] ← u[1..T-1] + u_prev[T-1] ← u[T-1] ← hold last action +``` + +--- + +## 6.3 Private Methods + +### 6.3.1 `_get_nearest_waypoint(x, y, update_prev_idx)` + +```python +def _get_nearest_waypoint(self, x, y, update_prev_idx=False): + view = _StateView(x, y, 0.0, 0.0) + nearest_idx = self.course.search_nearest_point_index(view) + ref_x = self.course.point_x_m(nearest_idx) + ref_y = self.course.point_y_m(nearest_idx) + ref_yaw = self.course.point_yaw_rad(nearest_idx) + ref_v = self.course.point_speed_mps(nearest_idx) + if update_prev_idx: + self.prev_waypoints_idx = nearest_idx + return ref_x, ref_y, ref_yaw, ref_v +``` + +This method queries the course for the nearest point to `(x, y)` using a global nearest-neighbour search over all course points. It returns the reference position, heading, and speed at that point. If `update_prev_idx=True`, the found index is stored — this is called once per `update()` step on the vehicle's actual position to anchor the cost lookups. Note that during rollouts, this method is called on the **simulated** position of each sample, not the vehicle's real position. + +--- + +### 6.3.2 `_g(v)` — Control Clipping + +```python +def _g(self, v): + steer = np.clip(v[0], -self.max_steer_abs, self.max_steer_abs) + accel = np.clip(v[1], -self.max_accel_abs, self.max_accel_abs) + return np.array([steer, accel]) +``` + +After adding noise to the warm-start control, `_g()` clips the result to the physical limits of the vehicle. This is MPPI's only mechanism for enforcing control bounds — there are no hard constraints as in MPC. Samples that would require `|δ| > δ_max` are simply clipped to the limit before being rolled out. + +--- + +### 6.3.3 `_F(x_t, v_t)` — One-Step Dynamics Rollout + +```python +def _F(self, x_t, v_t): + x, y, yaw, v = x_t[0,0], x_t[1,0], x_t[2,0], x_t[3,0] + steer, accel = float(v_t[0]), float(v_t[1]) + if abs(v) < 1e-9: + yaw_rate = 0.0 + else: + yaw_rate = v / self.WHEEL_BASE_M * tan(steer) + state_vec = np.array([[x], [y], [yaw], [v]]) + input_vec = np.array([[accel], [yaw_rate]]) + return State.motion_model(state_vec, input_vec, self.delta_t) +``` + +This method advances the vehicle state by one time step `Δt` using the kinematic bicycle model. It converts the MPPI control `(steer, accel)` into the `(accel, yaw_rate)` format expected by `State.motion_model`: + +``` +yaw_rate = v / L · tan(δ) +``` + +A guard against near-zero speed (`|v| < 1e-9`) is included to avoid division by zero — the same guard used in the Stanley controller. This function is called `K × T` times per `update()` step — once for each sample, at each horizon step — making it the computational hotspot of the algorithm. + +--- + +### 6.3.4 `_c(x_t)` — Stage Cost + +```python +def _c(self, x_t): + x, y, yaw, v = float(x_t[0,0]), float(x_t[1,0]), float(x_t[2,0]), float(x_t[3,0]) + yaw = (yaw + 2.0 * np.pi) % (2.0 * np.pi) + ref_x, ref_y, ref_yaw, ref_v = self._get_nearest_waypoint(x, y) + ref_yaw = (ref_yaw + 2.0 * np.pi) % (2.0 * np.pi) + yaw_diff = np.arctan2(np.sin(yaw - ref_yaw), np.cos(yaw - ref_yaw)) + cost = ( stage_cost_weight[0] * (x - ref_x)**2 + + stage_cost_weight[1] * (y - ref_y)**2 + + stage_cost_weight[2] * yaw_diff**2 + + stage_cost_weight[3] * (v - ref_v)**2 ) + return cost +``` + +The stage cost measures how far the **simulated** state `x_t` of sample `k` at step `t` deviates from the nearest reference point on the course. It is evaluated at every step of every rollout. + +The yaw is normalised to `[0, 2π)` before computing the heading difference. `atan2(sin(·), cos(·))` then maps the difference back to `(−π, π]`, giving the shortest-path angle error regardless of which quadrant the vehicle is in. + +The nearest reference point is found by a **global search** — unlike the MPC controller which uses an arc-length scaled forward search, MPPI calls `_get_nearest_waypoint()` with the simulated position at each rollout step. This works well because MPPI's rollouts are typically short and stay near the course. + +--- + +### 6.3.5 `_phi(x_T)` — Terminal Cost + +```python +def _phi(self, x_T): + # Same structure as _c(), uses terminal_cost_weight + cost = ( terminal_cost_weight[0] * (x - ref_x)**2 + + terminal_cost_weight[1] * (y - ref_y)**2 + + terminal_cost_weight[2] * yaw_diff**2 + + terminal_cost_weight[3] * (v - ref_v)**2 ) + return cost +``` + +The terminal cost is evaluated only at the **last state** `x_T^k` of each sample rollout. It uses `terminal_cost_weight` instead of `stage_cost_weight`. By default the weights are the same, but setting the terminal weights higher encourages samples to end up in a good state at the end of the horizon — analogous to the terminal cost in MPC. + +--- + +### 6.3.6 `_calc_epsilon()` + +```python +def _calc_epsilon(self): + mu = np.zeros(2) + epsilon = np.random.multivariate_normal(mu, self.Sigma, (self.K, self.T)) + return epsilon +``` + +Samples the entire noise matrix $ε ∈ ℝ^{K × T × 2}$ in a single vectorised call using numpy's `multivariate_normal`. The shape $(K, T, 2)$ ` means: K samples, each of T steps, each with 2-dimensional noise ` +$[\epsilon_{steer}, \epsilon_{accel}]$. + +The covariance matrix is: + +$$ +Σ = diag(σ_steer², σ_accel²) = [[σ_steer², 0 ], + [ 0, σ_accel²]] +$$ + +The off-diagonal zeros mean steering and acceleration noise are drawn independently. A single `multivariate_normal` call is used instead of two separate `normal` calls to keep the code consistent with the MPPI formulation, which always refers to a single noise vector $\epsilon_t ∈ ℝ²$. + +--- + +### 6.3.7 `_compute_weights(S)` + +```python +def _compute_weights(self, S): + rho = S.min() + eta = np.sum(np.exp((-1.0 / self.param_lambda) * (S - rho))) + w = (1.0 / eta) * np.exp((-1.0 / self.param_lambda) * (S - rho)) + return w +``` + +Implements the information-theoretic weighting formula in three lines. The subtraction of $ρ = min(S)$ before the exponential is not a mathematical change, it is a **numerical stability trick**. Without it, $exp(−S/λ)$ would underflow to $0$ for all samples when $S$ values are large, making the weights undefined. Subtracting $ρ$ ensures the best sample always contributes $exp(0) = 1.0$ to the numerator. + +The output $w$ is a $(K,)$ array that sums to $1.0$, acting as a proper probability distribution over the $K$ samples. + +--- + +### 6.3.8 `_moving_average_filter(xx, window_size)` + +```python +def _moving_average_filter(self, xx, window_size): + b = np.ones(window_size) / window_size + xx_mean = np.zeros_like(xx) + for d in range(xx.shape[1]): + xx_mean[:, d] = np.convolve(xx[:, d], b, mode="same") + n_conv = math.ceil(window_size / 2) + xx_mean[0, d] *= window_size / n_conv + for i in range(1, n_conv): + xx_mean[i, d] *= window_size / (i + n_conv) + xx_mean[-i, d] *= window_size / (i + n_conv - (window_size % 2)) + return xx_mean +``` + +Applies a moving-average filter to each column of the $(T, 2)$ weighted perturbation array $w_ε$. The filter smooths the control sequence along the time dimension, reducing high-frequency chatter that can occur when different samples pull the update in opposite directions at adjacent timesteps. + +The edge correction loop rescales the beginning and end of the filtered signal where the convolution window extends beyond the available data - `numpy`'s `mode="same"` convolves with zero-padding at the edges, which effectively down-weights the first and last few values. The correction counteracts this by scaling them back up proportionally. + +--- + +## 6.4 Public Methods + +### 6.4.1 `update` + +```python +def update(self, state, time_s): + if not self.course: return + + x0 = np.array([[state.get_x_m()], + [state.get_y_m()], + [state.get_yaw_rad()], + [state.get_speed_mps()]]) + + self._get_nearest_waypoint(x0[0,0], x0[1,0], update_prev_idx=True) + u = self.u_prev.copy() + epsilon = self._calc_epsilon() + + # Build v_{k,t} — perturbed controls for each sample + n_exploit = int((1.0 - self.param_exploration) * self.K) + for k in range(self.K): + for t in range(self.T): + v[k,t] = self._g(u[t] + epsilon[k,t]) if k < n_exploit \ + else self._g(epsilon[k,t]) + + # Rollout and cost accumulation + Sigma_inv = np.linalg.inv(self.Sigma) + for k in range(self.K): + x = x0.copy() + for t in range(self.T): + S[k] += self._c(x) + self.param_gamma * (u[t].T @ Sigma_inv @ v[k,t]) + x = self._F(x, v[k,t]) + S[k] += self._phi(x) + + w = self._compute_weights(S) + w_epsilon = sum_k( w[k] * epsilon[k] ) # shape (T, 2) + w_epsilon = self._moving_average_filter(w_epsilon, window) + u = clip( u_prev + w_epsilon ) + + self.target_steer_rad = u[0, 0] + self.target_accel_mps2 = u[0, 1] + self.target_yaw_rate_rps = v0 / L * tan(steer0) + + self.u_prev[:-1] = u[1:] + self.u_prev[-1] = u[-1] +``` + +This is the main entry point called every simulation frame by `FourWheelsVehicle`. It orchestrates all private methods in order: + +``` +update() + 1. Build x0 (4×1) from state getters + 2. Anchor nearest waypoint index (update_prev_idx=True) + 3. Copy u_prev as warm-start nominal sequence u + 4. Sample ε ∈ ℝ^{K×T×2} via _calc_epsilon() + 5. Build v_{k,t} — exploitation or exploration + clip via _g() + 6. For each sample k: + For each step t: + S[k] += _c(x) + γ · u[t]ᵀ Σ⁻¹ v[k,t] + x = _F(x, v[k,t]) + S[k] += _phi(x) + 7. w = _compute_weights(S) + 8. w_ε[t] = Σ_k w[k] · ε[k,t] for all t + 9. Optional: _moving_average_filter(w_ε) + 10. u = clip(u_prev + w_ε) + 11. target_steer, target_accel ← u[0] + 12. target_yaw_rate = v / L · tan(steer) + 13. Store optimal and sampled trajectories for draw() + 14. Shift warm start: u_prev[0..T-2] ← u[1..T-1] +``` + +If no course is set, the method returns early without computing anything. + +--- + +### 6.4.2 Getter Methods + +```python +def get_target_accel_mps2(self): + return self.target_accel_mps2 + +def get_target_yaw_rate_rps(self): + return self.target_yaw_rate_rps + +def get_target_steer_rad(self): + return self.target_steer_rad +``` + +These three getter methods expose the computed control outputs. They are called by `FourWheelsVehicle` after each `update()` to apply the commands to the vehicle's state. The interface is identical to `StanleyController` and `MpcController`. + +--- + +### 6.4.3 `draw` + +```python +def draw(self, axes, elems): + if self.visualize_sampled_trajs and self.sampled_trajectories: + for (x_list, y_list), w in zip(self.sampled_trajectories, self.weights): + alpha = 0.06 + 0.12 * min(1.0, float(w) * self.K) + (line,) = axes.plot(x_list, y_list, "b-", linewidth=0.35, alpha=alpha) + elems.append(line) + + if self.visualize_optimal_traj and self.optimal_trajectory: + x_list, y_list = self.optimal_trajectory + (line,) = axes.plot(x_list, y_list, color=self.DRAW_COLOR, + linewidth=2.0, alpha=0.9, label="MPPI trajectory") + elems.append(line) +``` + +Unlike the Stanley controller where `draw()` is an empty `pass`, and unlike the MPC controller which draws one line, MPPI draws **two layers of visual information**: + +**Sampled trajectories** - all $K$ rollouts are drawn as thin blue lines. Each line's transparency ($\alpha$) is scaled by the sample's weight: + +$$ +alpha = 0.06 + 0.12 · min(1.0, w_k · K) +$$ + +A sample with average weight ($w_k = 1/K$) gets $\alpha ≈ 0.18$. A sample with $5×$ average weight gets $alpha = 0.66$. This makes the most-influential samples visually prominent and the low-weight samples nearly invisible - you can literally see which trajectories the controller is paying attention to(darker). + +**Optimal trajectory** - a single solid line in the constructor colour (default green) showing the optimal control sequence $u$ rolled out from the current state. This is the trajectory the controller has committed to executing, not just the best single sample - it is the weighted-average result after all K samples are combined. + +--- + +## 6.5 Comparison with MPC + +| Aspect | MPC | MPPI | +|----------|----------|----------| +| Optimization Method | Deterministic nonlinear optimization | Sampling-based stochastic optimization | +| Error Used | Heading + position + speed | Heading + position + speed | +| Prediction Horizon | N-step deterministic trajectory | T-step horizon with K sampled rollouts | +| Constraints | Hard constraints enforced by solver | Soft constraints via clipping and cost penalties | +| Solver | IPOPT / SQP / QP solver | No explicit solver; weighted rollout averaging | +| Compute Cost | ~10–50 ms (CPU, IPOPT) | ~1–5 ms (CPU), <1 ms (GPU) | +| Local Minima | Can get trapped in local minima | Better exploration through stochastic sampling | +| Tuning Parameters | Cost weights and horizon length | $\lambda$, $K$, $\sigma$, $\alpha$, horizon length | +| Dynamics Requirement | Requires differentiable model | Can use arbitrary black-box dynamics | +| Parallelization | Limited | Highly parallelizable (GPU-friendly) | +| Visualization | Single predicted optimal trajectory | All sampled rollouts plus optimal trajectory | +| Robustness to Model Errors | Sensitive to model mismatch | More robust due to sampling-based exploration | +| Real-Time Performance | Depends on solver convergence | Fixed computational budget via sample count | + +--- + +**Author**: Mohit Kumar diff --git a/doc/4_path_tracking/mppi.png b/doc/4_path_tracking/mppi.png new file mode 100644 index 0000000000000000000000000000000000000000..1389446f41903e1b761e15861ada882f8bfc4d9e GIT binary patch literal 142125 zcmeFZhdvlXT6y=V3=9OE485JE^o_MXSy>zK*jj+t|^ImhNW zw%^P3eqYz+`h4I2!SCvJOWk;#=j$==kNFg&q##XBN=J%^he!VQjg$%=9%1qMUy{qf zcQAL$`hkB4onF3GBLV(+keGhL!(+yKEA>Ln&0uw!I6)0D{c}s46^=8IdU5?eO~zZQ z)pyMSH$pk%Zl%BF4dQxM@*r;PmaKuTvdJnn-`=%gU!P##;533E$OZ{%eZRL$Al%8- ze8PWvVYO$&0a>6(OYr9{&IRLO`8!#}b}HLqNjpgZF>G zT)`)JtU&mGKLQ*BO(7yF_7xLRyY~MaNGaii7vl8)zKBwS*%F^1KZSxKO7egFl(}j4 z&i^2fl0?S`k7K!2a^ZgwPg4Ax_{)LBs;o*Wq{HN6UH+%Ru zd$?GO{##u9TU=adg8T~)|H8w+@NmH){+mMl|Gv0jCL^k}o%rexX?t?lN1~9F(r5u4 zVbOT~LW03;L)0g{A%dAG%eF(-3)|f0)bH zt+Ll{XkTR{Aej%wQ}!slW3_klV$xTtW3)C|F`6kGBjpAfd_{ZRzzjQRzC_s}iVrqg zKt8hw{|5tPL}5Z`R(gKh?|k;Z#?5=<%=oNFI$GeSF+k5)lV}+l1(QxXd3}KsX#RD# zg(%G%GwPW#QMN-FZ$D~kJDE;gC!~A>i0H$Uf3~uzwT#+=sa=NNvOL`Tof^nk`ba)b z?KeCX4Ya%^!1w`_S*|nc7u`>S*$v9%wcjk{vTDg=-cx*nL!o|#GckE2<}w2<@?uef z-KI%KJ~6+4G|UN^H~YO$d9E9*)nW?y9p^jntfi=dY5-b#J%w0fqPkS$AMZ$!BU|06sB3kJbt3n;7p$({GZ6=WEH6fs;j&qjHPwtu%|3eG$gkq=r zCc?`pe)%eyGW>Rvyf&x?PyRK=BVT~c3~GP}9U23v3tGS@Y`LjbW**AmvDq9HtnYn- z{ir#^aL@W99)Z~d35tAfU$csTOcf-o$nJRV4(^TWI%tQ`aNba4%Gar|wsP>r-UaOK z1)w@2cdZ<%e@r31us^jPVHKYLB6&lSc&R_xp*N1tkK~+<+W;GjJ*no>7uiTjBi$cp zZoMY_Soy^J1Y`J7lNu|b;y_3_ZV%X=od5&VKRM`d^3q>e6^nhMOtGqhSYIRu>5Yk;TN+W*{=N$TrLYmKS+TWU6Yq~@s2bdDF-aUvi z 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ðLUi>VHD%eHfU$l)%owC~(=toF=mA2sYj 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
    }oB~VT-v3kgob?Y7PpQrR&ZL4MklzWkXfe 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 C9*%zi literal 0 HcmV?d00001 From 0c33548dda39fdb8b913e136ab4299081f70ef01 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:14:11 +0530 Subject: [PATCH 02/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 48 +++++++++-------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index b71bb71..81c6234 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -185,39 +185,29 @@ CasADi writes these equations as symbolic expressions, meaning it can automatica At every time step, MPC solves the optimization problem: $$ -\begin{aligned} -\min_{U}\quad -J -&= +\min_{U}\quad J = \sum_{t=0}^{N-1} \ell(x_t,u_t,u_{t-1}) + \phi(x_N) -\\[6pt] -\text{subject to}\quad -& -x_{t+1} -= -f(x_t,u_t), -\qquad t=0,\ldots,N-1 -\\[4pt] -& -|\delta_t| -\le -\delta_{\max} -\\[4pt] -& -|a_t| -\le -a_{\max} -\\[4pt] -& -v_{\min} -\le -v_t -\le -v_{\max} -\end{aligned} +$$ + +subject to + +$$ +x_{t+1}=f(x_t,u_t) +$$ + +$$ +|\delta_t| \le \delta_{\max} +$$ + +$$ +|a_t| \le a_{\max} +$$ + +$$ +v_{\min} \le v_t \le v_{\max} $$ where: From 0f193541b34ac19cd46647538e86a557925fda92 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:17:12 +0530 Subject: [PATCH 03/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 81c6234..abf607a 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -238,7 +238,7 @@ w_y (p_{y,t} - p_{y,r})^2 + w_\psi \left[ -\operatorname{atan2} +\mathrm{atan2} \left( \sin(\psi_t-\psi_r), \cos(\psi_t-\psi_r) @@ -251,7 +251,7 @@ $$ The heading error uses $$ -\operatorname{atan2} +\mathrm{atan2} \left( \sin(\psi_t-\psi_r), \cos(\psi_t-\psi_r) @@ -322,7 +322,7 @@ W_y (p_{y,N} - p_{y,rN})^2 + W_\psi \left[ -\operatorname{atan2} +\mathrm{atan2} \left( \sin(\psi_N-\psi_{rN}), \cos(\psi_N-\psi_{rN}) From 4f2e20984f38e0a6129420752622137470e2cc1e Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:18:52 +0530 Subject: [PATCH 04/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index abf607a..f3cb16c 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -338,7 +338,7 @@ The terminal weights $W$ are set heavier than the stage weights (default: $W = 2 ### 5.2.6 The Receding Horizon Principle -MPC solves for the full sequence ${u₀, u₁, …, u_{N-1}}$ but **only applies $u₀$** - the first control action. At the next time step, the horizon shifts forward by one step, the current state is re-measured, and the problem is solved again from scratch. This is why it is called a **receding horizon controller**. +MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only applies $u₀$** - the first control action. At the next time step, the horizon shifts forward by one step, the current state is re-measured, and the problem is solved again from scratch. This is why it is called a **receding horizon controller**. At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: From 4b6819352ee2696fed383d58d9a5c407cd52fa26 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:23:04 +0530 Subject: [PATCH 05/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index f3cb16c..048af6f 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -238,7 +238,7 @@ w_y (p_{y,t} - p_{y,r})^2 + w_\psi \left[ -\mathrm{atan2} +\text{atan2} \left( \sin(\psi_t-\psi_r), \cos(\psi_t-\psi_r) @@ -251,7 +251,7 @@ $$ The heading error uses $$ -\mathrm{atan2} +\text{atan2} \left( \sin(\psi_t-\psi_r), \cos(\psi_t-\psi_r) @@ -532,10 +532,12 @@ This method builds the $(4, T+1)$ reference array that is injected into the NLP The formula for each reference step is: -``` -target_s = s₀ + k × v × Δt +$$ +{target_s} = s₀ + k × v × Δt +$$ +$$ ref[k] = course point nearest to arc-length target_s -``` +$$ where $s₀$ is the cumulative arc-length at the vehicle's current nearest point, and $v × Δt$ is the distance travelled per prediction step. From 427e03b31dd3663a1e6b7422dd6d55cd77283898 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:27:45 +0530 Subject: [PATCH 06/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 048af6f..cb8e38f 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -167,10 +167,10 @@ MPC uses an internal motion model to predict how the vehicle will move in respon $$ \begin{aligned} -p_{x,t+1} &= p_{x,t} + v_t \cos(\psi_t)\,\Delta t, \\ -p_{y,t+1} &= p_{y,t} + v_t \sin(\psi_t)\,\Delta t, \\ -\psi_{t+1} &= \psi_t + \frac{v_t}{L}\tan(\delta_t)\,\Delta t, \\ -v_{t+1} &= v_t + a_t\,\Delta t. +p_{x,t+1} &= p_{x,t} + v_t \cos(\psi_t)\ \Delta t \\ +p_{y,t+1} &= p_{y,t} + v_t \sin(\psi_t)\ \Delta t \\ +\psi_{t+1} &= \psi_t + \frac{v_t}{L}\tan(\delta_t)\ \Delta t \\ +v_{t+1} &= v_t + a_t\,\Delta t \end{aligned} $$ @@ -230,20 +230,20 @@ The stage cost is evaluated at every step $t = 0,\ldots,N-1$ of the horizon. It Penalises deviation from the reference trajectory: $$ -\ell_{\text{track}} +\ell_{\mathrm{track}} = -w_x (p_{x,t} - p_{x,r})^2 +w_x (p_{x,t}-p_{x,r})^2 + -w_y (p_{y,t} - p_{y,r})^2 +w_y (p_{y,t}-p_{y,r})^2 + w_\psi -\left[ -\text{atan2} -\left( +\Bigl[ +\operatorname{atan2} +\bigl( \sin(\psi_t-\psi_r), \cos(\psi_t-\psi_r) -\right) -\right]^2 +\bigr) +\Bigr]^2 + w_v (v_t-v_r)^2 $$ From 673f5e2322c49408e89a9176849f12db7c858b0c Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:29:33 +0530 Subject: [PATCH 07/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index cb8e38f..2ed5c3e 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -236,16 +236,20 @@ w_x (p_{x,t}-p_{x,r})^2 + w_y (p_{y,t}-p_{y,r})^2 + -w_\psi -\Bigl[ +w_\psi e_\psi^2 ++ +w_v (v_t-v_r)^2 +$$ + +where + +$$ +e_\psi = \operatorname{atan2} -\bigl( +\!\left( \sin(\psi_t-\psi_r), \cos(\psi_t-\psi_r) -\bigr) -\Bigr]^2 -+ -w_v (v_t-v_r)^2 +\right). $$ The heading error uses From fd47c0ed6e38e9758847bebbf007c978a7b8d061 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:30:29 +0530 Subject: [PATCH 08/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 2ed5c3e..dec86f8 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -245,7 +245,7 @@ where $$ e_\psi = -\operatorname{atan2} +\text{atan2} \!\left( \sin(\psi_t-\psi_r), \cos(\psi_t-\psi_r) From 552a26fb075bf366b8c0787c993da988734e4116 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:36:10 +0530 Subject: [PATCH 09/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 28 ++--------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index dec86f8..c11fa8b 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -230,15 +230,7 @@ The stage cost is evaluated at every step $t = 0,\ldots,N-1$ of the horizon. It Penalises deviation from the reference trajectory: $$ -\ell_{\mathrm{track}} -= -w_x (p_{x,t}-p_{x,r})^2 -+ -w_y (p_{y,t}-p_{y,r})^2 -+ -w_\psi e_\psi^2 -+ -w_v (v_t-v_r)^2 +\ell_{\mathrm{track}} = w_x (p_{x,t}-p_{x,r})^2 + w_y (p_{y,t}-p_{y,r})^2 + w_\psi e_\psi^2 + w_v (v_t-v_r)^2 $$ where @@ -252,23 +244,7 @@ e_\psi = \right). $$ -The heading error uses - -$$ -\text{atan2} -\left( -\sin(\psi_t-\psi_r), -\cos(\psi_t-\psi_r) -\right) -$$ - -rather than a direct subtraction. This maps the error into: - -$$ -(-\pi,\pi] -$$ - -and remains smooth when the angle wraps around $\pm\pi$. +The heading error uses **$\text{atan2}$** rather than a direct subtraction. This maps the error into $(-\pi,\pi]$, and remains smooth when the angle wraps around $\pm\pi$. --- From 4a6288458f6bbfec1d3f5fa130dad9f7b519ce96 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:37:27 +0530 Subject: [PATCH 10/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index c11fa8b..08eec6e 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -237,11 +237,7 @@ where $$ e_\psi = -\text{atan2} -\!\left( -\sin(\psi_t-\psi_r), -\cos(\psi_t-\psi_r) -\right). +\text{atan2}\!\left(\sin(\psi_t-\psi_r),\cos(\psi_t-\psi_r)\right). $$ The heading error uses **$\text{atan2}$** rather than a direct subtraction. This maps the error into $(-\pi,\pi]$, and remains smooth when the angle wraps around $\pm\pi$. From b003a365234fe31fbbc61074448d2794a689d78c Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:40:07 +0530 Subject: [PATCH 11/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 08eec6e..60f3e8a 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -237,7 +237,7 @@ where $$ e_\psi = -\text{atan2}\!\left(\sin(\psi_t-\psi_r),\cos(\psi_t-\psi_r)\right). +\mathrm{atan2}\!\left(\sin(\psi_t-\psi_r),\cos(\psi_t-\psi_r)\right). $$ The heading error uses **$\text{atan2}$** rather than a direct subtraction. This maps the error into $(-\pi,\pi]$, and remains smooth when the angle wraps around $\pm\pi$. From 74ccba8e15faaec025fd155a5d64f30daff59bb8 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:40:50 +0530 Subject: [PATCH 12/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 60f3e8a..2b93bcf 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -237,7 +237,7 @@ where $$ e_\psi = -\mathrm{atan2}\!\left(\sin(\psi_t-\psi_r),\cos(\psi_t-\psi_r)\right). +\mathrm{atan2}\left(\sin(\psi_t-\psi_r),\cos(\psi_t-\psi_r)\right). $$ The heading error uses **$\text{atan2}$** rather than a direct subtraction. This maps the error into $(-\pi,\pi]$, and remains smooth when the angle wraps around $\pm\pi$. From c3243ed91a0b58354ec33238f7e5fb9a6a05fdfe Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:42:20 +0530 Subject: [PATCH 13/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 26 +++-------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 2b93bcf..c0df25b 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -249,11 +249,7 @@ The heading error uses **$\text{atan2}$** rather than a direct subtraction. This Penalises large control inputs: $$ -\ell_{\text{effort}} -= -w_\delta \,\delta_t^2 -+ -w_a\,a_t^2 +\ell_{\text{effort}} = w_\delta \,\delta_t^2 + w_a\,a_t^2 $$ --- @@ -263,13 +259,7 @@ $$ Penalises rapid changes between consecutive control inputs: $$ -\ell_{\text{smooth}} -= -w_{\Delta\delta} -(\delta_t-\delta_{t-1})^2 -+ -w_{\Delta a} -(a_t-a_{t-1})^2 +\ell_{\text{smooth}} = w_{\Delta\delta}(\delta_t-\delta_{t-1})^2 + w_{\Delta a} (a_t-a_{t-1})^2 $$ This is implemented via do-mpc's `set_rterm()` which automatically adds this term at every horizon step. It prevents the controller from producing jerky steering commands. @@ -290,17 +280,7 @@ mpc.set_rterm( The terminal cost is evaluated only at the **last step** $t = N$ of the horizon: $$ -\phi(x_N) -= -W_x (p_{x,N} - p_{x,rN})^2 -+ -W_y (p_{y,N} - p_{y,rN})^2 -+ -W_\psi -\left[ -\mathrm{atan2} -\left( -\sin(\psi_N-\psi_{rN}), +\phi(x_N) = W_x (p_{x,N} - p_{x,rN})^2 + W_y (p_{y,N} - p_{y,rN})^2 + W_\psi\left[\mathrm{atan2}\left(\sin(\psi_N-\psi_{rN}), \cos(\psi_N-\psi_{rN}) \right) \right]^2 From 28cf0101a3476bb49c1c217e668634a25c59af04 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:43:55 +0530 Subject: [PATCH 14/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index c0df25b..861381c 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -299,28 +299,12 @@ MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only appl At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: $$ -U^* -= -\left\{ -u_0^*,\,u_1^*,\,\ldots,\,u_{N-1}^* -\right\} +U^* = \left\{u_0^*,\,u_1^*,\,\ldots,\,u_{N-1}^*\right\} $$ -but only +**but** only $u(k)=u_0^*$ is applied to the vehicle. -$$ -u(k)=u_0^* -$$ - -is applied to the vehicle. - -At the next sampling instant, the optimization is solved again using the updated state estimate: - -$$ -x(k+1) -$$ - -resulting in a new optimal control sequence. +At the next sampling instant, the optimization is solved again using the updated state estimate $x(k+1)$ resulting in a new optimal control sequence. This strategy is known as the **receding horizon** (or **moving horizon**) principle. From 8561dcd24fc8d00573ab666430a1f3676ba84598 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:46:22 +0530 Subject: [PATCH 15/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 861381c..21a0a2f 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -299,7 +299,7 @@ MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only appl At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: $$ -U^* = \left\{u_0^*,\,u_1^*,\,\ldots,\,u_{N-1}^*\right\} +U^* = \{u_0^*, u_1^*, \ldots, u_{N-1}^*\} $$ **but** only $u(k)=u_0^*$ is applied to the vehicle. From 88cc150eb40368fec175dd4824ed4af63e2a7f82 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:51:33 +0530 Subject: [PATCH 16/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 21a0a2f..04778e8 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -299,7 +299,7 @@ MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only appl At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: $$ -U^* = \{u_0^*, u_1^*, \ldots, u_{N-1}^*\} +U^{*} = \{u_0^*, u_1^*, \ldots, u_{N-1}^*\} $$ **but** only $u(k)=u_0^*$ is applied to the vehicle. @@ -473,13 +473,22 @@ This method builds the $(4, T+1)$ reference array that is injected into the NLP The formula for each reference step is: $$ -{target_s} = s₀ + k × v × Δt +s_{\text{target}} += +s_0 ++ +k\,v\,\Delta t $$ $$ -ref[k] = course point nearest to arc-length target_s +\mathrm{ref}[k] += +\arg\min_{p_i} +\left| +s_i - s_{\text{target}} +\right| $$ -where $s₀$ is the cumulative arc-length at the vehicle's current nearest point, and $v × Δt$ is the distance travelled per prediction step. +where $s_0$ is the cumulative arc-length at the vehicle's current nearest point, and $v × Δt$ is the distance travelled per prediction step. --- From 4273bfa1e5ea61c5e96bca8a1d0ea2887e0fd842 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:53:33 +0530 Subject: [PATCH 17/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 04778e8..2faae34 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -294,7 +294,7 @@ The terminal weights $W$ are set heavier than the stage weights (default: $W = 2 ### 5.2.6 The Receding Horizon Principle -MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only applies $u₀$** - the first control action. At the next time step, the horizon shifts forward by one step, the current state is re-measured, and the problem is solved again from scratch. This is why it is called a **receding horizon controller**. +MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only applies $u_₀$** - the first control action. At the next time step, the horizon shifts forward by one step, the current state is re-measured, and the problem is solved again from scratch. This is why it is called a **receding horizon controller**. At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: From 5192bdb9b5460be1ec7d7e8682e41aaa419145a4 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:54:32 +0530 Subject: [PATCH 18/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 2faae34..2d4933d 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -299,7 +299,7 @@ MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only appl At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: $$ -U^{*} = \{u_0^*, u_1^*, \ldots, u_{N-1}^*\} +U^* = \{u_0^*, u_1^*, \ldots, u_{N-1}^*\} $$ **but** only $u(k)=u_0^*$ is applied to the vehicle. From d0d8b8f14a47f68d3fba0d11bf9bb9558ddd4eac Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:55:45 +0530 Subject: [PATCH 19/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 2d4933d..43fa2aa 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -299,7 +299,7 @@ MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only appl At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: $$ -U^* = \{u_0^*, u_1^*, \ldots, u_{N-1}^*\} +{U^* = \{u_0^*, u_1^*, \ldots, u_{N-1}^*\}} $$ **but** only $u(k)=u_0^*$ is applied to the vehicle. From eee08684f0d1620f472a4713106b7e4b9db4617c Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 22:57:33 +0530 Subject: [PATCH 20/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 43fa2aa..ff7038b 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -157,7 +157,7 @@ $$ -At each time step, MPC finds the sequence $U = {u₀, u₁, …, u_{N-1}}$ that minimises the total cost $J$ over the horizon. +At each time step, MPC finds the sequence $U = {u_0, u_1, …, u_{N-1}}$ that minimises the total cost $J$ over the horizon. --- @@ -473,19 +473,11 @@ This method builds the $(4, T+1)$ reference array that is injected into the NLP The formula for each reference step is: $$ -s_{\text{target}} -= -s_0 -+ -k\,v\,\Delta t +s_{\text{target}} = s_0 + k\,v\,\Delta t $$ + $$ -\mathrm{ref}[k] -= -\arg\min_{p_i} -\left| -s_i - s_{\text{target}} -\right| +\mathrm{ref}[k] = \arg\min_{p_i}\left|s_i - s_{\text{target}}\right| $$ where $s_0$ is the cumulative arc-length at the vehicle's current nearest point, and $v × Δt$ is the distance travelled per prediction step. From 63bc66b4ca7537aabe066f288a26f864a7e85a71 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 23:05:58 +0530 Subject: [PATCH 21/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index ff7038b..006f0e9 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -299,7 +299,7 @@ MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only appl At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: $$ -{U^* = \{u_0^*, u_1^*, \ldots, u_{N-1}^*\}} +U^* = u_0^*, u_1^*, \ldots, u_{N-1}^* $$ **but** only $u(k)=u_0^*$ is applied to the vehicle. From 0d1017a070a618ff0803cae8c96d95919b3e6420 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 23:07:45 +0530 Subject: [PATCH 22/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 006f0e9..12680c3 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -299,7 +299,7 @@ MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only appl At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: $$ -U^* = u_0^*, u_1^*, \ldots, u_{N-1}^* +U^{*} = u_0^{*}, u_1^{*}, \ldots, u_{N-1}^{*} $$ **but** only $u(k)=u_0^*$ is applied to the vehicle. From c55d4b5284855f141486d85674a04dacb9e26aab Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 23:09:22 +0530 Subject: [PATCH 23/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 12680c3..57aac1c 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -299,7 +299,7 @@ MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only appl At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: $$ -U^{*} = u_0^{*}, u_1^{*}, \ldots, u_{N-1}^{*} +U^{*} = u_0^{*}, u_1^{*}, {\ldots}, u_{N-1}^{*} $$ **but** only $u(k)=u_0^*$ is applied to the vehicle. From a938926f71b395e0c25dc76b9e479d827b7bc106 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 23:13:41 +0530 Subject: [PATCH 24/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 57aac1c..6bf9fb5 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -299,7 +299,7 @@ MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only appl At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: $$ -U^{*} = u_0^{*}, u_1^{*}, {\ldots}, u_{N-1}^{*} +U^{*} = u_0^{*}, u_1^{*}, .... , u_{N-1}^{*} $$ **but** only $u(k)=u_0^*$ is applied to the vehicle. From 27157d88776970d61e9d4fe660f07ffcab32a85b Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 23:14:35 +0530 Subject: [PATCH 25/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 6bf9fb5..90afcb7 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -299,7 +299,7 @@ MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only appl At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: $$ -U^{*} = u_0^{*}, u_1^{*}, .... , u_{N-1}^{*} +U^{\ast} = u_0^{\ast},\ u_1^{\ast},\ \ldots,\ u_{N-1}^{\ast} $$ **but** only $u(k)=u_0^*$ is applied to the vehicle. From c24ed9dee9c64e2bae24e680c0b7299e1d6c0499 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 8 Jun 2026 23:15:07 +0530 Subject: [PATCH 26/29] chore: render --- doc/4_path_tracking/4_2_mpc_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_2_mpc_controller.md b/doc/4_path_tracking/4_2_mpc_controller.md index 90afcb7..5f68bca 100644 --- a/doc/4_path_tracking/4_2_mpc_controller.md +++ b/doc/4_path_tracking/4_2_mpc_controller.md @@ -294,7 +294,7 @@ The terminal weights $W$ are set heavier than the stage weights (default: $W = 2 ### 5.2.6 The Receding Horizon Principle -MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only applies $u_₀$** - the first control action. At the next time step, the horizon shifts forward by one step, the current state is re-measured, and the problem is solved again from scratch. This is why it is called a **receding horizon controller**. +MPC solves for the full sequence $\{u_0, u_1, \ldots, u_{N-1}\}$ but **only applies $u_0$** - the first control action. At the next time step, the horizon shifts forward by one step, the current state is re-measured, and the problem is solved again from scratch. This is why it is called a **receding horizon controller**. At each control cycle, MPC solves an optimization problem over the prediction horizon, but only the first control action is applied: From ee1ed73b7dafab10731c9eb35151e57319a17174 Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Sun, 14 Jun 2026 18:41:21 +0530 Subject: [PATCH 27/29] docs(): added mppi controller doc --- doc/4_path_tracking/4_3_mppi_controller.md | 410 +++++++++--------- .../control/mppi/mppi_controller.py | 4 +- .../mpc_path_tracking/mpc_path_tracking.py | 9 +- 3 files changed, 217 insertions(+), 206 deletions(-) diff --git a/doc/4_path_tracking/4_3_mppi_controller.md b/doc/4_path_tracking/4_3_mppi_controller.md index 4faf8be..9d47cbf 100644 --- a/doc/4_path_tracking/4_3_mppi_controller.md +++ b/doc/4_path_tracking/4_3_mppi_controller.md @@ -18,7 +18,7 @@ The key idea is: run $K$ imagined futures in parallel, score each by how well it 4. **Update** - shift the nominal control sequence toward the best samples

    - MPPI overview + MPPI overview

    Ref: https://dilithjay.com/blog/mppi @@ -32,9 +32,9 @@ Ref: https://dilithjay.com/blog/mppi **Disadvantages:** -- Constraints are **soft only** - satisfied by increasing cost, never hard-blocked(like MPC where hard constraints are inforced). +- Constraints are **soft only** - satisfied by increasing cost, never hard-blocked (like MPC where hard constraints are enforced). - Solution quality depends on K - more samples give a better approximation but cost more compute. -- Tuning parameters($λ$, $K$, $σ$) requires experimentation with no systematic design method. +- Tuning parameters ($\lambda$, $K$, $\sigma$) requires experimentation with no systematic design method. - Can be noisy at low K values, producing jittery control. --- @@ -71,7 +71,30 @@ This class imports trigonometric functions from Python's `math` module and `nump --- -### 6.1.1 Constructor +### 6.1.1 `_StateView` Helper Class + +```python +class _StateView: + """Minimal state-like object for course methods (x, y, yaw, speed).""" + + def __init__(self, x_m, y_m, yaw_rad, speed_mps): + self.x_m = x_m + self.y_m = y_m + ... + + def get_x_m(self): return self.x_m + def get_y_m(self): return self.y_m + def get_yaw_rad(self): return self.yaw_rad + def get_speed_mps(self): return self.speed_mps +``` + +`_StateView` is a lightweight internal adapter class. The course object's `search_nearest_point_index()` method expects an object with four getter methods - `get_x_m()`, `get_y_m()`, `get_yaw_rad()`, and `get_speed_mps()` - matching the interface of the real `State` class. + +During rollouts, the simulated position of each sample is a plain numpy array, not a `State` object. `_StateView` wraps any `(x, y, yaw, speed)` tuple into the interface the course expects, without allocating a full `State` object. It is not a public class and is only used internally by `_get_nearest_waypoint()`. + +--- + +### 6.1.2 Constructor ```python def __init__(self, spec, course=None, color="g", @@ -87,18 +110,18 @@ def __init__(self, spec, course=None, color="g", visualize_optimal_traj=True, visualize_sampled_trajs=True): - self.WHEEL_BASE_M = spec.wheel_base_m - self.T = horizon_step_T - self.K = number_of_samples_K - self.param_lambda = max(1e-6, param_lambda) - self.param_gamma = param_lambda * (1.0 - param_alpha) - self.Sigma = np.array([[sigma_steer**2, 0.0], - [0.0, sigma_accel**2]]) - self.stage_cost_weight = np.asarray(stage_cost_weight - or [50.0, 50.0, 1.0, 20.0]) + self.WHEEL_BASE_M = spec.wheel_base_m + self.T = horizon_step_T + self.K = number_of_samples_K + self.param_lambda = max(1e-6, param_lambda) + self.param_gamma = param_lambda * (1.0 - param_alpha) + self.Sigma = np.array([[sigma_steer**2, 0.0], + [0.0, sigma_accel**2]]) + self.stage_cost_weight = np.asarray(stage_cost_weight + or [50.0, 50.0, 1.0, 20.0]) self.terminal_cost_weight = np.asarray(terminal_cost_weight or self.stage_cost_weight.copy()) - self.u_prev = np.zeros((self.T, 2)) + self.u_prev = np.zeros((self.T, 2)) ``` The constructor takes a `VehicleSpecification` object and an optional `CubicSplineCourse`. The key member variables are: @@ -108,15 +131,18 @@ The constructor takes a `VehicleSpecification` object and an optional `CubicSpli | `WHEEL_BASE_M` | from spec | Distance between front and rear axles [m] | | `T` | 20 | Prediction horizon - number of steps rolled out per sample | | `K` | 256 | Number of sample trajectories drawn each step | -| `param_lambda` | 50.0 | Temperature $λ$ - controls sharpness of weighting | -| `param_gamma` | $λ(1−α)$ | Control cost scaling; $α=1 → γ=0$ (no control cost) | -| `Sigma` | diag$(σ_δ², σ_a²)$ | Noise covariance matrix for sampling | -| `stage_cost_weight` | [50, 50, 1, 20] | $[w_x, w_y, w_ψ, w_v]$ - stage tracking weights | -| `terminal_cost_weight` | same as stage | $[w_x, w_y, w_ψ, w_v]$ - terminal tracking weights | +| `param_lambda` | 50.0 | Temperature $\lambda$ - controls sharpness of weighting | +| `param_gamma` | $\lambda(1-\alpha)$ | Control cost scaling; default $\alpha=1.0 \Rightarrow \gamma=0$ (cost term off) | +| `Sigma` | $\text{diag}(\sigma_\delta^2, \sigma_a^2)$ | Noise covariance matrix for sampling | +| `stage_cost_weight` | [50, 50, 1, 20] | $[w_x, w_y, w_\psi, w_v]$ - stage tracking weights | +| `terminal_cost_weight` | same as stage | $[w_x, w_y, w_\psi, w_v]$ - terminal tracking weights | | `u_prev` | zeros (T, 2) | Warm-start control sequence `[steer, accel]` per step | | `target_accel_mps2` | 0.0 | Computed acceleration command [m/s²] | | `target_steer_rad` | 0.0 | Computed steering angle command [rad] | | `target_yaw_rate_rps` | 0.0 | Computed yaw rate command [rad/s] | +| `target_speed_mps` | 0.0 | Current speed echoed back (not a planned target - see §6.4.1) | + +> **Note on `param_gamma`**: With the default `param_alpha = 1.0`, `param_gamma = 0` and the control cost term in the trajectory cost vanishes entirely. This means the default configuration is pure tracking with no penalty for deviating from the warm-start control. Set `param_alpha < 1.0` only when you want to discourage large perturbations from the previous solution. --- @@ -127,191 +153,130 @@ The constructor takes a `VehicleSpecification` object and an optional `CubicSpli MPPI operates on a state vector $x_t$ and control vector $u_t$: $$ -x_t -= -\begin{bmatrix} -x \\ -y \\ -\psi \\ -v -\end{bmatrix} -\qquad -\text{(position [m], heading [rad], speed [m/s])} +x_t = \begin{bmatrix} x \\ y \\ \psi \\ v \end{bmatrix} +\qquad \text{(position [m], heading [rad], speed [m/s])} $$ $$ -u_t -= -\begin{bmatrix} -\delta \\ -a -\end{bmatrix} -\qquad -\text{(steering angle [rad], acceleration [m/s2])} +u_t = \begin{bmatrix} \delta \\ a \end{bmatrix} +\qquad \text{(steering angle [rad], acceleration [m/s²])} $$ The **nominal control sequence** (warm-started from the previous control cycle) is stored as: $$ -U -= -\left\{ -u_{\text{prev}}[0], -u_{\text{prev}}[1], -\ldots, -u_{\text{prev}}[T-1] -\right\} -$$ - -with shape: - -$$ -(T,\,2) +U = \left\{ u_{\text{prev}}[0],\; u_{\text{prev}}[1],\; \ldots,\; u_{\text{prev}}[T-1] \right\} \quad \text{shape: } (T,\,2) $$ --- -### 6.2.2 Theoretical Foundation - Free Energy and KL Divergence +### 6.2.2 Sampling - Exploitation and Exploration -MPPI is derived from the principle of minimising a **free energy** objective. The theoretical result shows that the optimal control update under a Gaussian prior is equivalent to minimising: +At each step, K noise sequences are drawn from a zero-mean Gaussian: $$ -J = E_Q[S(τ)] + λ · D_KL(Q ‖ P₀) +\varepsilon_{k,t} \;\sim\; \mathcal{N}(0, \Sigma) \qquad k = 1\ldots K,\quad t = 0\ldots T-1 $$ -where -- $S(τ)$ is the trajectory cost -- $Q$ is the sampling distribution --$P₀$ is the uncontrolled prior -- $D_KL$ is the KL divergence measuring how far $Q$ departs from $P_0$. - -The closed-form solution to this gives the information-theoretic weight for each sample: $$ -w_k ∝ exp( −S(k) / λ ) +\Sigma = \mathrm{diag}(\sigma_{\mathrm{steer}}^2,\; \sigma_{\mathrm{accel}}^2) +\qquad \text{(steer and accel noise are independent)} $$ -This is exactly the **softmin formula**: lower-cost samples receive exponentially higher weight. The temperature parameter $λ$ controls the sharpness: - - -- $λ → 0$ : only the single best sample contributes (greedy / deterministic) -- $λ → ∞$ : all samples receive equal weight (fully random) - - ---- - -### 6.2.3 Sampling - Exploitation and Exploration - -At each step, K noise sequences are drawn from a zero-mean Gaussian: - -``` -ε_{k,t} ~ N(0, Σ) for k = 1…K, t = 0…T-1 - -Σ = diag(σ_steer², σ_accel²) (diagonal — steer and accel noise are independent) -``` - The K samples are split into two groups: ``` -Exploitation samples (first (1 − param_exploration) × K): +Exploitation samples (first (1 − param_exploration) × K): v_{k,t} = clip( u_prev[t] + ε_{k,t} ) ← perturb warm start Exploration samples (last param_exploration × K): v_{k,t} = clip( ε_{k,t} ) ← pure random, ignores warm start ``` -The exploitation samples stay close to the previous solution and refine it. The exploration samples venture further afield and can discover better solutions when the warm start has drifted off-course. The `param_exploration` parameter controls the ratio. +The exploitation samples refine the previous solution. The exploration samples venture further afield and can discover better solutions when the warm start has drifted off-course. The `param_exploration` parameter controls the ratio. + +> **Important**: `v[k,t]` stores the **clipped** perturbed control used for rollout. The raw unclipped noise `epsilon[k,t]` is stored separately. The weighted update later uses `epsilon`, not `v` - this asymmetry is intentional and explained in section 6.2.5. --- -### 6.2.4 Trajectory Cost S(k) +### 6.2.3 Trajectory Cost S(k) -Each sample trajectory `k` accumulates cost across all T steps plus a terminal cost: +Each sample trajectory $k$ accumulates cost across all T steps plus a terminal cost: -``` -S(k) = Σ_{t=0}^{T-1} [ c(x_t^k) + γ · u_prev[t]ᵀ Σ⁻¹ v_{k,t} ] + ϕ(x_T^k) -``` +$$ +S(k) = \sum_{t=0}^{T-1} \left[ c(x_t^k) \;+\; \gamma \cdot u_{\text{prev}}[t]^\top \Sigma^{-1} v_{k,t} \right] \;+\; \phi(x_T^k) +$$ -**Stage tracking cost** `c(x)` — deviation from the reference path: +**Stage tracking cost** $c(x)$ - deviation from the reference path: -``` -c(x) = w_x · (x − x_r)² - + w_y · (y − y_r)² - + w_ψ · atan2(sin(ψ − ψ_r), cos(ψ − ψ_r))² - + w_v · (v − v_r)² -``` +$$ +c(x) = w_x(x - x_r)^2 + w_y(y - y_r)^2 + w_\psi\,\mathrm{atan2}(\sin(\psi - \psi_r), \cos(\psi - \psi_r))^2 + w_v(v - v_r)^2 +$$ -The heading error uses `atan2(sin(·), cos(·))` for the same reason as MPC — it wraps the angle difference into `(−π, π]` and avoids discontinuities near ±π. +The heading error uses `atan2(sin(·), cos(·))` - it wraps the angle difference into $(-\pi, \pi]$ and avoids discontinuities near $\pm\pi$. -**Control cost term** `γ · u_prev[t]ᵀ Σ⁻¹ v_{k,t}` — this term is the mathematical signature of the MPPI formulation. It penalises samples that deviate far from the warm-start control `u_prev`. The parameter `γ = λ(1 − α)` controls its strength: +**Control cost term** $\gamma \cdot u_{\text{prev}}[t]^\top \Sigma^{-1} v_{k,t}$ - penalises samples that deviate far from the warm-start control. The parameter $\gamma = \lambda(1 - \alpha)$ controls its strength: ``` -param_alpha = 1.0 → γ = 0 (control cost off — pure tracking) -param_alpha = 0.0 → γ = λ (full control cost — conservative) +param_alpha = 1.0 → γ = 0 (control cost off - pure tracking, default) +param_alpha = 0.0 → γ = λ (full control cost - conservative) ``` -In practice `param_alpha = 0.98` or `1.0` works well for path tracking. - -**Terminal cost** `ϕ(x_T)` — same structure as `c(x)` but evaluated only at the last step of each rollout, using `terminal_cost_weight` instead of `stage_cost_weight`. +**Terminal cost** $\phi(x_T^k)$ - same structure as $c(x)$ but evaluated only at the last rollout step, using `terminal_cost_weight` instead of `stage_cost_weight`. --- -### 6.2.5 Information-Theoretic Weighting +### 6.2.4 Information-Theoretic Weighting Once all K trajectory costs are computed, the weights are calculated in three steps: -**Step 1** — Subtract the minimum cost for numerical stability (prevents `exp` overflow): +**Step 1** - Subtract the minimum cost for numerical stability (prevents `exp` overflow): -``` -ρ = min_k S(k) -``` +$$\rho = \min_k S(k)$$ -**Step 2** — Compute unnormalised softmin weights: +**Step 2** - Compute unnormalised softmin weights: -``` -η̃_k = exp( −(S(k) − ρ) / λ ) -``` +$$\tilde{\eta}_k = \exp\!\left( -\frac{S(k) - \rho}{\lambda} \right)$$ -**Step 3** — Normalise so weights sum to 1: +**Step 3** - Normalise so weights sum to 1: -``` -η = Σ_k η̃_k -w_k = η̃_k / η -``` +$$\eta = \sum_k \tilde{\eta}_k \qquad w_k = \frac{\tilde{\eta}_k}{\eta}$$ -The result is a proper probability distribution over the K samples. Samples with cost close to `ρ` (the best sample) get weight near `1/K` or higher; samples with cost far above `ρ` get weight near 0. +The subtraction of $\rho$ before the exponential is a **numerical stability trick** - not a mathematical change. Without it, $\exp(-S/\lambda)$ would underflow to $0$ for all samples when $S$ values are large, making all weights undefined. Subtracting $\rho$ ensures the best sample always contributes $\exp(0) = 1.0$ to the numerator. --- -### 6.2.6 Control Update +### 6.2.5 Control Update - Why `epsilon` Not `v` -The weighted perturbation sum is computed across all K samples for each horizon step: +The weighted perturbation sum is computed using the **raw unclipped noise** `epsilon`, not the clipped `v`: -``` -w_ε[t] = Σ_k w_k · ε_{k,t} shape: (T, 2) -``` +$$ +w_\varepsilon[t] = \sum_k w_k \cdot \varepsilon_{k,t} \qquad \text{shape: } (T, 2) +$$ -This is the information-theoretically optimal update direction — a weighted combination of all noise perturbations, pulled toward the samples that worked best. +**Why `epsilon` and not `v`?** +If the weighted average used the clipped `v[k,t]` instead of the raw `epsilon[k,t]`, the clipping would introduce a systematic bias. Samples that hit the steering or acceleration limit would all contribute the same clipped value regardless of how far outside the limit their original noise was - pulling the update asymmetrically toward the constraint boundary. Using the raw `epsilon` means the weighting reflects the true stochastic intent of each sample without distortion from the clipping operation. -**Optional smoothing** — a moving-average filter can be applied to `w_ε` along the time axis: +**Optional smoothing** - a moving-average filter can be applied to $w_\varepsilon$ along the time axis: -``` -w_ε ← MovingAverage(w_ε, window) -``` +$$w_\varepsilon \;\leftarrow\; \mathrm{MovingAverage}(w_\varepsilon,\; \text{window})$$ This reduces chatter in the control sequence at the cost of slightly slower response. **Final update and warm-start shift:** ``` -u = clip( u_prev + w_ε ) ← updated control sequence +u = clip( u_prev + w_ε ) <- updated control sequence -apply u[0] → steer0, accel0 ← first action executed this step +apply u[0] → steer0, accel0 <- first action executed this step warm-start shift for next step: - u_prev[0..T-2] ← u[1..T-1] - u_prev[T-1] ← u[T-1] ← hold last action + u_prev[0..T-2] ← u[1..T-1] <- shift sequence forward by one + u_prev[T-1] ← u[T-1] <- hold last action (not zero) ``` +The last element is **repeated rather than zeroed** because zeroing would force a sudden discontinuity in the control sequence at the horizon boundary - the vehicle would plan to abruptly stop accelerating or steering at step T. Holding the last value is a smoother and more conservative assumption about what happens beyond the horizon. + --- ## 6.3 Private Methods @@ -331,11 +296,18 @@ def _get_nearest_waypoint(self, x, y, update_prev_idx=False): return ref_x, ref_y, ref_yaw, ref_v ``` -This method queries the course for the nearest point to `(x, y)` using a global nearest-neighbour search over all course points. It returns the reference position, heading, and speed at that point. If `update_prev_idx=True`, the found index is stored — this is called once per `update()` step on the vehicle's actual position to anchor the cost lookups. Note that during rollouts, this method is called on the **simulated** position of each sample, not the vehicle's real position. +This method queries the course for the nearest point to `(x, y)` using a global nearest-neighbour search over all course points. The `_StateView(x, y, 0.0, 0.0)` wrapper is required because `search_nearest_point_index()` expects an object with getter methods, not a plain tuple. + +It is called in two distinct contexts: + +- **Once per `update()` step** on the vehicle's actual position with `update_prev_idx=True` - anchors the stored index. +- **K × T times during rollouts** on the simulated position of each sample - uses a fresh global search each time. + +> **Computational note**: Each call to `_get_nearest_waypoint()` performs a full O(N) scan over all N course points. With K=256 samples and T=20 steps, this method is called **5120 times per `update()` step**. On a course with N=500 points, that is 2.56 million distance comparisons per control cycle. This is the primary reason MPPI is slower than expected on CPU and why GPU batching (vectorising the rollout across K) reduces compute from O(K×T×N) sequential calls to a single matrix operation. --- -### 6.3.2 `_g(v)` — Control Clipping +### 6.3.2 `_g(v)` - Control Clipping ```python def _g(self, v): @@ -344,11 +316,13 @@ def _g(self, v): return np.array([steer, accel]) ``` -After adding noise to the warm-start control, `_g()` clips the result to the physical limits of the vehicle. This is MPPI's only mechanism for enforcing control bounds — there are no hard constraints as in MPC. Samples that would require `|δ| > δ_max` are simply clipped to the limit before being rolled out. +After adding noise to the warm-start control, `_g()` clips the result to the physical limits of the vehicle. This is MPPI's only mechanism for enforcing control bounds - there are no hard constraints as in MPC. Samples that would require $|\delta| > \delta_{\max}$ are simply clipped to the limit before being rolled out. + +The clipped result is stored in `v[k,t]`. Critically, the raw unclipped noise `epsilon[k,t]` is preserved separately and is used in the weighted update - see section 6.2.6 for why this separation matters. --- -### 6.3.3 `_F(x_t, v_t)` — One-Step Dynamics Rollout +### 6.3.3 `_F(x_t, v_t)` - One-Step Dynamics Rollout ```python def _F(self, x_t, v_t): @@ -363,17 +337,17 @@ def _F(self, x_t, v_t): return State.motion_model(state_vec, input_vec, self.delta_t) ``` -This method advances the vehicle state by one time step `Δt` using the kinematic bicycle model. It converts the MPPI control `(steer, accel)` into the `(accel, yaw_rate)` format expected by `State.motion_model`: +This method advances the vehicle state by one time step $\Delta t$ using the kinematic bicycle model. It exists as a bridge layer because `State.motion_model` takes `(accel, yaw_rate)` as its input, but MPPI works in `(steer, accel)` space. `_F` performs the conversion: -``` -yaw_rate = v / L · tan(δ) -``` +$$\dot{\psi} = \frac{v}{L} \tan(\delta)$$ + +> A guard against near-zero speed ($|v| < 10^{-9}$) prevents division by zero. -A guard against near-zero speed (`|v| < 1e-9`) is included to avoid division by zero — the same guard used in the Stanley controller. This function is called `K × T` times per `update()` step — once for each sample, at each horizon step — making it the computational hotspot of the algorithm. +This function is called $K \times T$ times per `update()` step - once for each sample at each horizon step - making it the **computational hotspot** of the algorithm alongside `_c()`. --- -### 6.3.4 `_c(x_t)` — Stage Cost +### 6.3.4 `_c(x_t)` - Stage Cost ```python def _c(self, x_t): @@ -389,15 +363,18 @@ def _c(self, x_t): return cost ``` -The stage cost measures how far the **simulated** state `x_t` of sample `k` at step `t` deviates from the nearest reference point on the course. It is evaluated at every step of every rollout. +The stage cost measures how far the **simulated** state $x_t^k$ deviates from the nearest reference point on the course. It is evaluated at every step of every rollout. -The yaw is normalised to `[0, 2π)` before computing the heading difference. `atan2(sin(·), cos(·))` then maps the difference back to `(−π, π]`, giving the shortest-path angle error regardless of which quadrant the vehicle is in. +**Two-step yaw normalisation**: the yaw is first mapped to $[0, 2\pi)$ using `% (2π)`, and then `atan2(sin(·), cos(·))` maps the difference back to $(-\pi, \pi]$. The two steps serve different purposes: -The nearest reference point is found by a **global search** — unlike the MPC controller which uses an arc-length scaled forward search, MPPI calls `_get_nearest_waypoint()` with the simulated position at each rollout step. This works well because MPPI's rollouts are typically short and stay near the course. +1. `% (2π)` - makes both `yaw` and `ref_yaw` positive before subtraction, preventing a sign mismatch from wrap-around (e.g. vehicle at 350° and reference at 10° giving a −340° difference instead of +20°). +2. `atan2(sin(·), cos(·))` - maps the already-consistent difference to the shortest angular path in $(-\pi, \pi]$. + +Without the first step, vehicles near $\pm\pi$ can receive arbitrarily large heading errors from a simple sign difference. --- -### 6.3.5 `_phi(x_T)` — Terminal Cost +### 6.3.5 `_phi(x_T)` - Terminal Cost ```python def _phi(self, x_T): @@ -409,7 +386,7 @@ def _phi(self, x_T): return cost ``` -The terminal cost is evaluated only at the **last state** `x_T^k` of each sample rollout. It uses `terminal_cost_weight` instead of `stage_cost_weight`. By default the weights are the same, but setting the terminal weights higher encourages samples to end up in a good state at the end of the horizon — analogous to the terminal cost in MPC. +The terminal cost is evaluated only at the **last state** $x_T^k$ of each sample rollout. It uses `terminal_cost_weight` instead of `stage_cost_weight`. By default the weights are the same, but setting the terminal weights higher encourages samples to end up in a good state at the end of the horizon - analogous to the terminal cost in MPC. --- @@ -422,17 +399,15 @@ def _calc_epsilon(self): return epsilon ``` -Samples the entire noise matrix $ε ∈ ℝ^{K × T × 2}$ in a single vectorised call using numpy's `multivariate_normal`. The shape $(K, T, 2)$ ` means: K samples, each of T steps, each with 2-dimensional noise ` -$[\epsilon_{steer}, \epsilon_{accel}]$. +Samples the entire noise matrix $\varepsilon \in \mathbb{R}^{K \times T \times 2}$ in a single vectorised call. The shape $(K, T, 2)$ means: K samples, each of T steps, each with 2-dimensional noise $[\varepsilon_{\mathrm{steer}},\, \varepsilon_{\mathrm{accel}}]$. The covariance matrix is: $$ -Σ = diag(σ_steer², σ_accel²) = [[σ_steer², 0 ], - [ 0, σ_accel²]] +\Sigma = \mathrm{diag}(\sigma_{\mathrm{steer}}^2,\; \sigma_{\mathrm{accel}}^2) = \begin{bmatrix} \sigma_{\mathrm{steer}}^2 & 0 \\ 0 & \sigma_{\mathrm{accel}}^2 \end{bmatrix} $$ -The off-diagonal zeros mean steering and acceleration noise are drawn independently. A single `multivariate_normal` call is used instead of two separate `normal` calls to keep the code consistent with the MPPI formulation, which always refers to a single noise vector $\epsilon_t ∈ ℝ²$. +The off-diagonal zeros mean steering and acceleration noise are drawn independently. A single `multivariate_normal` call is used rather than two separate `normal` calls to stay consistent with the MPPI formulation, which always refers to a single noise vector $\varepsilon_t \in \mathbb{R}^2$. --- @@ -446,9 +421,9 @@ def _compute_weights(self, S): return w ``` -Implements the information-theoretic weighting formula in three lines. The subtraction of $ρ = min(S)$ before the exponential is not a mathematical change, it is a **numerical stability trick**. Without it, $exp(−S/λ)$ would underflow to $0$ for all samples when $S$ values are large, making the weights undefined. Subtracting $ρ$ ensures the best sample always contributes $exp(0) = 1.0$ to the numerator. +Implements the information-theoretic weighting formula in three lines. The subtraction of $\rho = \min(S)$ before the exponential is a **numerical stability trick**, not a mathematical change. Without it, $\exp(-S/\lambda)$ would underflow to 0 for all samples when $S$ values are large, making all weights undefined. Subtracting $\rho$ ensures the best sample always contributes $\exp(0) = 1.0$ to the numerator. -The output $w$ is a $(K,)$ array that sums to $1.0$, acting as a proper probability distribution over the $K$ samples. +The output $w$ is a $(K,)$ array that sums to 1.0, acting as a proper probability distribution over the K samples. These weights are also stored as `self.weights = w.tolist()` for use by `draw()` to scale each sampled trajectory's transparency. --- @@ -468,9 +443,9 @@ def _moving_average_filter(self, xx, window_size): return xx_mean ``` -Applies a moving-average filter to each column of the $(T, 2)$ weighted perturbation array $w_ε$. The filter smooths the control sequence along the time dimension, reducing high-frequency chatter that can occur when different samples pull the update in opposite directions at adjacent timesteps. +Applies a moving-average filter to each column of the $(T, 2)$ weighted perturbation array $w_\varepsilon$. The filter smooths the control sequence along the time dimension, reducing high-frequency chatter. -The edge correction loop rescales the beginning and end of the filtered signal where the convolution window extends beyond the available data - `numpy`'s `mode="same"` convolves with zero-padding at the edges, which effectively down-weights the first and last few values. The correction counteracts this by scaling them back up proportionally. +**Edge correction**: `numpy`'s `mode="same"` pads the signal with zeros at the boundaries. For a window of size $W$, the first element is computed from only $\lceil W/2 \rceil$ real values instead of $W$, so `numpy` effectively divides by $W$ when only $\lceil W/2 \rceil$ values exist under-weighting the edges. The correction loop rescales each boundary element back up by $W / \text{actual\_count}$. The `window_size % 2` term handles the asymmetry between even and odd window sizes, where the left and right boundary element counts differ by one. --- @@ -480,8 +455,20 @@ The edge correction loop rescales the beginning and end of the filtered signal w ```python def update(self, state, time_s): - if not self.course: return + if not self.course: + self.target_accel_mps2 = 0.0 + self.target_yaw_rate_rps = 0.0 + self.target_steer_rad = 0.0 + self.target_speed_mps = state.get_speed_mps() + self.optimal_trajectory = None + self.sampled_trajectories = [] + self.weights = [] + return +``` +If no course is set, the method returns early and resets all outputs to safe defaults: acceleration and steering to zero, trajectories to empty lists, and weights to an empty list. Callers that inspect `self.weights` or `self.sampled_trajectories` after a no-course call will receive empty containers, not stale values from the previous step. + +```python x0 = np.array([[state.get_x_m()], [state.get_y_m()], [state.get_yaw_rad()], @@ -491,14 +478,12 @@ def update(self, state, time_s): u = self.u_prev.copy() epsilon = self._calc_epsilon() - # Build v_{k,t} — perturbed controls for each sample n_exploit = int((1.0 - self.param_exploration) * self.K) for k in range(self.K): for t in range(self.T): v[k,t] = self._g(u[t] + epsilon[k,t]) if k < n_exploit \ else self._g(epsilon[k,t]) - # Rollout and cost accumulation Sigma_inv = np.linalg.inv(self.Sigma) for k in range(self.K): x = x0.copy() @@ -507,44 +492,65 @@ def update(self, state, time_s): x = self._F(x, v[k,t]) S[k] += self._phi(x) - w = self._compute_weights(S) - w_epsilon = sum_k( w[k] * epsilon[k] ) # shape (T, 2) - w_epsilon = self._moving_average_filter(w_epsilon, window) - u = clip( u_prev + w_epsilon ) + w = self._compute_weights(S) + self.weights = w.tolist() + + w_epsilon = np.zeros((self.T, 2)) + for t in range(self.T): + for k in range(self.K): + w_epsilon[t] += w[k] * epsilon[k, t] # ← epsilon, not v + + if self.moving_average_window >= 2: + w_epsilon = self._moving_average_filter(w_epsilon, self.moving_average_window) - self.target_steer_rad = u[0, 0] - self.target_accel_mps2 = u[0, 1] - self.target_yaw_rate_rps = v0 / L * tan(steer0) + u = np.clip(u + w_epsilon, + [-self.max_steer_abs, -self.max_accel_abs], + [ self.max_steer_abs, self.max_accel_abs]) + + self.target_steer_rad = float(u[0, 0]) + self.target_accel_mps2 = float(u[0, 1]) + v0 = state.get_speed_mps() + self.target_yaw_rate_rps = 0.0 if abs(v0) < 1e-9 \ + else v0 / self.WHEEL_BASE_M * tan(self.target_steer_rad) + self.target_speed_mps = v0 # echoes current speed, not a planned target self.u_prev[:-1] = u[1:] self.u_prev[-1] = u[-1] ``` +> **`target_speed_mps` note**: Unlike Stanley which runs a proportional speed controller (`Kp × speed_error`), MPPI does not plan a separate speed command. `target_speed_mps` is set to the current measured speed `v0`. Speed tracking is handled entirely through the cost function's $w_v$ weight - samples that deviate from the reference speed receive higher cost and lower weight, indirectly shaping the acceleration commands produced. + This is the main entry point called every simulation frame by `FourWheelsVehicle`. It orchestrates all private methods in order: ``` update() - 1. Build x0 (4×1) from state getters - 2. Anchor nearest waypoint index (update_prev_idx=True) - 3. Copy u_prev as warm-start nominal sequence u - 4. Sample ε ∈ ℝ^{K×T×2} via _calc_epsilon() - 5. Build v_{k,t} — exploitation or exploration + clip via _g() - 6. For each sample k: + 1. If no course: zero all outputs, clear trajectories and weights, return + 2. Build x0 (4×1) from state getters + 3. Anchor nearest waypoint index (update_prev_idx=True) + 4. Copy u_prev as warm-start nominal sequence u + 5. Sample ε ∈ ℝ^{K×T×2} via _calc_epsilon() + 6. Build v_{k,t} - exploitation or exploration + clip via _g() + (v is clipped; raw epsilon is preserved separately) + 7. Compute Sigma_inv = inv(Sigma) + 8. For each sample k: For each step t: S[k] += _c(x) + γ · u[t]ᵀ Σ⁻¹ v[k,t] x = _F(x, v[k,t]) S[k] += _phi(x) - 7. w = _compute_weights(S) - 8. w_ε[t] = Σ_k w[k] · ε[k,t] for all t - 9. Optional: _moving_average_filter(w_ε) - 10. u = clip(u_prev + w_ε) - 11. target_steer, target_accel ← u[0] - 12. target_yaw_rate = v / L · tan(steer) - 13. Store optimal and sampled trajectories for draw() - 14. Shift warm start: u_prev[0..T-2] ← u[1..T-1] + 9. w = _compute_weights(S) + 10. self.weights = w.tolist() <- stored for draw() + 11. w_ε[t] = Σ_k w[k] · ε[k,t] <- raw epsilon, not v + 12. Optional: _moving_average_filter(w_ε) + 13. u = clip(u_prev + w_ε) + 14. target_steer, target_accel <- u[0] + 15. target_yaw_rate = v / L · tan(steer) + 16. target_speed_mps <- current speed (echoed, not planned) + 17. Rollout optimal trajectory using updated u -> self.optimal_trajectory + 18. Rollout each sample k using v[k] -> self.sampled_trajectories + 19. Shift warm start: u_prev[0..T-2] ← u[1..T-1], u_prev[T-1] <- u[T-1] ``` -If no course is set, the method returns early without computing anything. +**Steps 17 and 18 produce different trajectories**: the optimal trajectory uses the updated `u` (after the weighted update), while the sampled trajectories use `v[k]` (the original perturbed controls before the update). They represent different things - the optimal trajectory is what the controller commits to; the sampled trajectories are the K explored futures used to compute it. --- @@ -561,7 +567,7 @@ def get_target_steer_rad(self): return self.target_steer_rad ``` -These three getter methods expose the computed control outputs. They are called by `FourWheelsVehicle` after each `update()` to apply the commands to the vehicle's state. The interface is identical to `StanleyController` and `MpcController`. +These three getter methods expose the computed control outputs. They are called by `FourWheelsVehicle` after each `update()` to apply the commands to the vehicle's state. --- @@ -582,38 +588,40 @@ def draw(self, axes, elems): elems.append(line) ``` -Unlike the Stanley controller where `draw()` is an empty `pass`, and unlike the MPC controller which draws one line, MPPI draws **two layers of visual information**: +MPPI draws **two layers of visual information**: -**Sampled trajectories** - all $K$ rollouts are drawn as thin blue lines. Each line's transparency ($\alpha$) is scaled by the sample's weight: +1. **Sampled trajectories** - all $K$ rollouts drawn as thin blue lines. Each line's transparency $\alpha$ is scaled by the sample's weight: $$ -alpha = 0.06 + 0.12 · min(1.0, w_k · K) +\alpha = 0.06 + 0.12 \cdot \min(1.0,\; w_k \cdot K) $$ -A sample with average weight ($w_k = 1/K$) gets $\alpha ≈ 0.18$. A sample with $5×$ average weight gets $alpha = 0.66$. This makes the most-influential samples visually prominent and the low-weight samples nearly invisible - you can literally see which trajectories the controller is paying attention to(darker). +A sample with average weight ($w_k = 1/K$) gets $\alpha \approx 0.18$. A sample with $5\times$ average weight gets $\alpha = 0.66$. This makes the most-influential samples visually prominent - you can literally see which trajectories the controller is paying attention to(getting more weights). -**Optimal trajectory** - a single solid line in the constructor colour (default green) showing the optimal control sequence $u$ rolled out from the current state. This is the trajectory the controller has committed to executing, not just the best single sample - it is the weighted-average result after all K samples are combined. +The `self.weights` consumed here are set inside `update()` as `self.weights = w.tolist()` immediately after `_compute_weights()`. They are stored as an instance attribute rather than passed as a return value so `draw()` can access them without changing the `update()` call signature. + +2. **Optimal trajectory** - a single solid line in the constructor colour (default green) showing the updated control sequence `u` rolled out from the current state. This is distinct from the sampled trajectories - it represents the weighted-average result after all K samples are combined, not any individual sample. --- ## 6.5 Comparison with MPC | Aspect | MPC | MPPI | -|----------|----------|----------| -| Optimization Method | Deterministic nonlinear optimization | Sampling-based stochastic optimization | -| Error Used | Heading + position + speed | Heading + position + speed | -| Prediction Horizon | N-step deterministic trajectory | T-step horizon with K sampled rollouts | -| Constraints | Hard constraints enforced by solver | Soft constraints via clipping and cost penalties | -| Solver | IPOPT / SQP / QP solver | No explicit solver; weighted rollout averaging | -| Compute Cost | ~10–50 ms (CPU, IPOPT) | ~1–5 ms (CPU), <1 ms (GPU) | -| Local Minima | Can get trapped in local minima | Better exploration through stochastic sampling | -| Tuning Parameters | Cost weights and horizon length | $\lambda$, $K$, $\sigma$, $\alpha$, horizon length | -| Dynamics Requirement | Requires differentiable model | Can use arbitrary black-box dynamics | -| Parallelization | Limited | Highly parallelizable (GPU-friendly) | -| Visualization | Single predicted optimal trajectory | All sampled rollouts plus optimal trajectory | -| Robustness to Model Errors | Sensitive to model mismatch | More robust due to sampling-based exploration | -| Real-Time Performance | Depends on solver convergence | Fixed computational budget via sample count | +|---|---|---| +| Optimization method | Deterministic nonlinear optimization (NLP) | Sampling-based stochastic optimization | +| Error used | Heading + position + speed | Heading + position + speed | +| Prediction horizon | N-step deterministic trajectory | T-step horizon with K sampled rollouts | +| Constraints | Hard - enforced by IPOPT solver | Soft - clipping + cost penalty only | +| Solver | IPOPT / interior point method | None - weighted rollout averaging | +| Local minima | Can get trapped | Better exploration via stochastic sampling | +| Tuning parameters | Cost weights + horizon length | $\lambda$, $K$, $\sigma$, $\alpha$, horizon length | +| Dynamics requirement | Requires differentiable model (CasADi) | Arbitrary black-box dynamics via `_F()` | +| Parallelisation | Limited | Highly parallelisable on GPUs - K rollouts are independent | +| Robustness to model errors | Sensitive to model mismatch | More robust due to sampling-based exploration | +| Real-time performance | Depends on solver convergence | Fixed budget - determined by K and T | +| Speed control | Cost weight $w_v$ + horizon planning | Cost weight $w_v$ only; `target_speed_mps` echoes current speed | +| Constraint mechanism | Box bounds passed to solver | `_g()` clips controls; raw `epsilon` used in update | --- -**Author**: Mohit Kumar +**Author**: Mohit Kumar \ No newline at end of file diff --git a/src/components/control/mppi/mppi_controller.py b/src/components/control/mppi/mppi_controller.py index 2d5963a..0fd1857 100644 --- a/src/components/control/mppi/mppi_controller.py +++ b/src/components/control/mppi/mppi_controller.py @@ -112,6 +112,7 @@ def __init__( self.visualize_sampled_trajs = visualize_sampled_trajs self.Sigma = np.array([[sigma_steer**2, 0.0], [0.0, sigma_accel**2]]) + self.Sigma_inv = np.linalg.inv(self.Sigma) # precompute inverse for efficiency self.stage_cost_weight = np.asarray( stage_cost_weight if stage_cost_weight is not None @@ -272,13 +273,12 @@ def update(self, state, time_s): v[k, t] = self._g(v[k, t]) S = np.zeros(self.K) - Sigma_inv = np.linalg.inv(self.Sigma) for k in range(self.K): x = x0.copy() for t in range(self.T): u_t = u[t] v_t = v[k, t] - S[k] += self._c(x) + self.param_gamma * (u_t.T @ Sigma_inv @ v_t) + S[k] += self._c(x) + self.param_gamma * (u_t.T @ self.Sigma_inv @ v_t) x = self._F(x, v_t) S[k] += self._phi(x) diff --git a/src/simulations/path_tracking/mpc_path_tracking/mpc_path_tracking.py b/src/simulations/path_tracking/mpc_path_tracking/mpc_path_tracking.py index 3142f3e..d96f287 100644 --- a/src/simulations/path_tracking/mpc_path_tracking/mpc_path_tracking.py +++ b/src/simulations/path_tracking/mpc_path_tracking/mpc_path_tracking.py @@ -12,6 +12,9 @@ import sys from pathlib import Path +import warnings +# to supress do-mpc warnings about the full version +warnings.filterwarnings("ignore", category=UserWarning) # ── Path setup (identical pattern to mppi_path_tracking.py) ─────────────── abs_dir_path = str(Path(__file__).absolute().parent) @@ -29,7 +32,7 @@ from state import State from four_wheels_vehicle import FourWheelsVehicle from cubic_spline_course import CubicSplineCourse -from mpc_controller import MPCController # ← do-mpc controller +from mpc_controller import MPCController # do-mpc controller show_plot = True @@ -46,7 +49,7 @@ def main(): ) # ── Reference course ───────────────────────────────────────────────── - # Identical waypoints to mppi_path_tracking.py so the comparison is fair. + # Identical waypoints to mppi_path_tracking.py for comparison. course = CubicSplineCourse( [0.0, 10.0, 25, 40, 50], [0.0, 4.0, -12, 20, -13], @@ -59,7 +62,7 @@ def main(): state = State(color=spec.color) # ── MPC controller ─────────────────────────────────────────────────── - # Parameters are chosen to be comparable to the MPPI configuration: + # Parameters are chosen to be comparable to the MPPI configuration: # delta_t and horizon give a similar prediction window (≈ 2 s). # Stage / terminal cost weights mirror the MPPI weights exactly so # both controllers optimise the same objective shape. From d93cfb7a5dc7c5433036af9892049487bda80b2e Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 15 Jun 2026 08:37:44 +0530 Subject: [PATCH 28/29] chore(): render --- doc/4_path_tracking/4_3_mppi_controller.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/doc/4_path_tracking/4_3_mppi_controller.md b/doc/4_path_tracking/4_3_mppi_controller.md index 9d47cbf..c839a6b 100644 --- a/doc/4_path_tracking/4_3_mppi_controller.md +++ b/doc/4_path_tracking/4_3_mppi_controller.md @@ -165,7 +165,7 @@ $$ The **nominal control sequence** (warm-started from the previous control cycle) is stored as: $$ -U = \left\{ u_{\text{prev}}[0],\; u_{\text{prev}}[1],\; \ldots,\; u_{\text{prev}}[T-1] \right\} \quad \text{shape: } (T,\,2) +U = \{u_{\text{prev}}[0],\; u_{\text{prev}}[1],\; \ldots,\; u_{\text{prev}}[T-1] \} \quad \text{shape: } (T,\,2) $$ --- @@ -187,10 +187,10 @@ The K samples are split into two groups: ``` Exploitation samples (first (1 − param_exploration) × K): - v_{k,t} = clip( u_prev[t] + ε_{k,t} ) ← perturb warm start + v_{k,t} = clip( u_prev[t] + ε_{k,t} ) <- perturb warm start Exploration samples (last param_exploration × K): - v_{k,t} = clip( ε_{k,t} ) ← pure random, ignores warm start + v_{k,t} = clip( ε_{k,t} ) <- pure random, ignores warm start ``` The exploitation samples refine the previous solution. The exploration samples venture further afield and can discover better solutions when the warm start has drifted off-course. The `param_exploration` parameter controls the ratio. @@ -218,8 +218,8 @@ The heading error uses `atan2(sin(·), cos(·))` - it wraps the angle difference **Control cost term** $\gamma \cdot u_{\text{prev}}[t]^\top \Sigma^{-1} v_{k,t}$ - penalises samples that deviate far from the warm-start control. The parameter $\gamma = \lambda(1 - \alpha)$ controls its strength: ``` -param_alpha = 1.0 → γ = 0 (control cost off - pure tracking, default) -param_alpha = 0.0 → γ = λ (full control cost - conservative) +param_alpha = 1.0 -> γ = 0 (control cost off - pure tracking, default) +param_alpha = 0.0 -> γ = λ (full control cost - conservative) ``` **Terminal cost** $\phi(x_T^k)$ - same structure as $c(x)$ but evaluated only at the last rollout step, using `terminal_cost_weight` instead of `stage_cost_weight`. @@ -236,7 +236,9 @@ $$\rho = \min_k S(k)$$ **Step 2** - Compute unnormalised softmin weights: -$$\tilde{\eta}_k = \exp\!\left( -\frac{S(k) - \rho}{\lambda} \right)$$ +$$ +\tilde{\eta}_k = \exp(-\frac{S(k)-\rho}{\lambda}) +$$ **Step 3** - Normalise so weights sum to 1: @@ -445,7 +447,7 @@ def _moving_average_filter(self, xx, window_size): Applies a moving-average filter to each column of the $(T, 2)$ weighted perturbation array $w_\varepsilon$. The filter smooths the control sequence along the time dimension, reducing high-frequency chatter. -**Edge correction**: `numpy`'s `mode="same"` pads the signal with zeros at the boundaries. For a window of size $W$, the first element is computed from only $\lceil W/2 \rceil$ real values instead of $W$, so `numpy` effectively divides by $W$ when only $\lceil W/2 \rceil$ values exist under-weighting the edges. The correction loop rescales each boundary element back up by $W / \text{actual\_count}$. The `window_size % 2` term handles the asymmetry between even and odd window sizes, where the left and right boundary element counts differ by one. +**Edge correction**: `numpy`'s `mode="same"` pads the signal with zeros at the boundaries. For a window of size $W$, the first element is computed from only $\lceil W/2 \rceil$ real values instead of $W$, so `numpy` effectively divides by $W$ when only $\lceil W/2 \rceil$ values exist under-weighting the edges. The correction loop rescales each boundary element back up by $W / actual\_count$. The `window_size % 2` term handles the asymmetry between even and odd window sizes, where the left and right boundary element counts differ by one. --- From 3287cba73b0c9dbec2ab7f4dc205fd1d70efe27a Mon Sep 17 00:00:00 2001 From: mohitks3000 Date: Mon, 15 Jun 2026 08:40:04 +0530 Subject: [PATCH 29/29] chore(): render --- doc/4_path_tracking/4_3_mppi_controller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/4_path_tracking/4_3_mppi_controller.md b/doc/4_path_tracking/4_3_mppi_controller.md index c839a6b..9fcbe57 100644 --- a/doc/4_path_tracking/4_3_mppi_controller.md +++ b/doc/4_path_tracking/4_3_mppi_controller.md @@ -500,7 +500,7 @@ If no course is set, the method returns early and resets all outputs to safe def w_epsilon = np.zeros((self.T, 2)) for t in range(self.T): for k in range(self.K): - w_epsilon[t] += w[k] * epsilon[k, t] # ← epsilon, not v + w_epsilon[t] += w[k] * epsilon[k, t] # epsilon, not v if self.moving_average_window >= 2: w_epsilon = self._moving_average_filter(w_epsilon, self.moving_average_window)