Chapter 1: Mobile Robot Modeling Scope and Assumptions

Lesson 4: Sensing–Estimation–Navigation Pipeline

This lesson formalizes the end-to-end autonomy loop that turns raw sensor data into safe motion: sensing produces measurements; estimation turns measurements into a state estimate with quantified error; navigation converts that estimate plus an environment representation into commands. We develop a compact mathematical model of the pipeline, derive the canonical weighted least-squares estimator from maximum-likelihood principles, and prove a practical stability bound showing why navigation performance depends on estimation error.

1. Big-Picture Pipeline and Modeling Interfaces

From Lessons 1–3, we already have: (i) a mobile robot state (pose and possibly velocities), and (ii) an environment representation (grid, features, or geometry). The remaining question is: how do we connect sensor signals to a navigation decision in a way that is mathematically well-posed and operationally robust?

We model the autonomy loop in discrete time \( k \in \mathbb{Z}_{\ge 0} \) with sampling interval \( \Delta t \gt 0 \). Let the robot state be \( \mathbf{x}_k \in \mathbb{R}^n \) (Lesson 2), control input \( \mathbf{u}_k \in \mathbb{R}^p \), and measurement vector \( \mathbf{y}_k \in \mathbb{R}^m \). A minimal pipeline model is:

\[ \begin{aligned} \text{Plant (motion):}\quad & \mathbf{x}_{k+1} = \mathbf{f}(\mathbf{x}_k,\mathbf{u}_k) + \mathbf{w}_k \\ \text{Sensors:}\quad & \mathbf{y}_k = \mathbf{h}(\mathbf{x}_k,\mathcal{E}) + \mathbf{v}_k \\ \text{Estimator:}\quad & \hat{\mathbf{x}}_k = \mathbf{g}(\hat{\mathbf{x}}_{k-1},\mathbf{u}_{k-1},\mathbf{y}_k) \\ \text{Navigation:}\quad & \mathbf{u}_k = \boldsymbol{\pi}(\hat{\mathbf{x}}_k,\mathcal{M},\mathbf{r}_k) \end{aligned} \]

Here \( \mathcal{E} \) denotes the physical environment, while \( \mathcal{M} \) is the internal environment representation (Lesson 3), and \( \mathbf{r}_k \) is the navigation objective (goal pose, waypoint, corridor, etc.). The additive terms \( \mathbf{w}_k \) and \( \mathbf{v}_k \) represent unmodeled disturbances and sensor noise.

flowchart TD
  S["Sensors: wheel encoders, IMU, LiDAR, camera"] --> P["Preprocess: sync, calibration, filtering"]
  P --> E["Estimation: x_hat + uncertainty"]
  E --> W["World model: map / costmap"]
  W --> N["Navigation: plan + command"]
  N --> A["Actuation: motors"]
  A -->|motion changes world view| S
        

Scope and assumptions for Chapter 1. In later chapters, you will implement full Bayesian filters and SLAM; in this chapter we adopt a controlled scope:

  • State is low-dimensional planar motion (pose and optionally velocity).
  • Map/model is treated as known or slowly varying (we focus on the interface).
  • Estimator is expressed as optimization (weighted least squares) or as a generic observer.
  • Navigation is an abstract policy that consumes estimates; advanced planners appear later.

2. Measurement Geometry from Frames and Environment Models

Sensors measure the world through the robot’s pose. In planar mobile robotics, a common pose is \( \mathbf{x} = (x, y, \theta)^\top \) (Lesson 2). Let \( \{W\} \) be a world frame and \( \{B\} \) the robot body frame. The homogeneous transform from body to world is:

\[ \mathbf{T}_{WB}(x,y,\theta)= \begin{bmatrix} \cos\theta & -\sin\theta & x \\ \sin\theta & \cos\theta & y \\ 0 & 0 & 1 \end{bmatrix}. \]

If a landmark point has coordinates \( \mathbf{p}^B \in \mathbb{R}^2 \) in body frame, then in world coordinates:

\[ \begin{bmatrix} \mathbf{p}^W \\ 1 \end{bmatrix} = \mathbf{T}_{WB} \begin{bmatrix} \mathbf{p}^B \\ 1 \end{bmatrix} \quad\Rightarrow\quad \mathbf{p}^W = \mathbf{R}(\theta)\,\mathbf{p}^B + \mathbf{t}, \]

with \( \mathbf{R}(\theta) \) the planar rotation and \( \mathbf{t}=(x,y)^\top \) the translation. This is the core bridge between the state and environment representations (Lesson 3): a grid cell index, a feature location, or a polygon edge becomes a predicted measurement through \( \mathbf{h}(\mathbf{x},\mathcal{E}) \).

Proposition (composition consistency). For any three frames \(A,B,C\):

\[ \mathbf{T}_{AC} = \mathbf{T}_{AB}\,\mathbf{T}_{BC}. \]

Proof. By definition, homogeneous transforms are affine maps: \( \tilde{\mathbf{p}}^A = \mathbf{T}_{AB}\tilde{\mathbf{p}}^B \) and \( \tilde{\mathbf{p}}^B = \mathbf{T}_{BC}\tilde{\mathbf{p}}^C \), where \( \tilde{\mathbf{p}} = [\mathbf{p}^\top\; 1]^\top \). Substituting gives \( \tilde{\mathbf{p}}^A = \mathbf{T}_{AB}\mathbf{T}_{BC}\tilde{\mathbf{p}}^C \). Since this holds for all points, the composed transform must be \( \mathbf{T}_{AC} = \mathbf{T}_{AB}\mathbf{T}_{BC} \). \( \square \)

This proposition is not an abstract detail: it is the mathematical mechanism behind sensor extrinsic calibration (sensor frame to body frame), map alignment (world frame choice), and multi-sensor fusion (consistent frame transformations).

3. Estimation as Maximum Likelihood and Weighted Least Squares

The estimator’s goal is to compute \( \hat{\mathbf{x}} \) from measurements. In this lesson we adopt a classical control/estimation view: a deterministic state \( \mathbf{x} \) is inferred from noisy observations. Consider a measurement model: \( \mathbf{y} = \mathbf{h}(\mathbf{x}) + \mathbf{v} \), with \( \mathbf{v} \sim \mathcal{N}(\mathbf{0},\mathbf{R}) \).

Theorem (Gaussian ML equals WLS). Under the Gaussian noise assumption above, the maximum-likelihood estimate satisfies:

\[ \hat{\mathbf{x}}_{\mathrm{ML}} = \arg\min_{\mathbf{x}} \left(\mathbf{y}-\mathbf{h}(\mathbf{x})\right)^\top \mathbf{R}^{-1} \left(\mathbf{y}-\mathbf{h}(\mathbf{x})\right). \]

Proof. The likelihood is \( p(\mathbf{y}\mid\mathbf{x}) \propto \exp\left(-\tfrac{1}{2}(\mathbf{y}-\mathbf{h}(\mathbf{x}))^\top \mathbf{R}^{-1}(\mathbf{y}-\mathbf{h}(\mathbf{x}))\right) \). Maximizing the likelihood is equivalent to minimizing the negative log-likelihood; the constant terms drop, leaving exactly the weighted least-squares objective. \( \square \)

Adding a prior (odometry / prediction). In mobile robots, we nearly always have a prediction \( \mathbf{x}^{-} \) from dead-reckoning (Chapter 5) or from a process model. We encode it as a quadratic penalty (a “soft constraint”):

\[ \hat{\mathbf{x}} = \arg\min_{\mathbf{x}} \underbrace{\left(\mathbf{x}-\mathbf{x}^{-}\right)^\top\mathbf{P}^{-1}\left(\mathbf{x}-\mathbf{x}^{-}\right)}_{\text{prior term}} + \underbrace{\left(\mathbf{y}-\mathbf{h}(\mathbf{x})\right)^\top\mathbf{R}^{-1}\left(\mathbf{y}-\mathbf{h}(\mathbf{x})\right)}_{\text{measurement term}}. \]

The matrix \( \mathbf{P} \) plays the role of a confidence/uncertainty matrix for the prior. Later chapters will interpret this probabilistically; here it is enough to view it as a weighting matrix with physical units.

Gauss–Newton step (local linearization). Linearize \( \mathbf{h}(\mathbf{x}) \approx \mathbf{h}(\mathbf{x}_0) + \mathbf{H}(\mathbf{x}_0)\,\delta \), where \( \mathbf{H} = \tfrac{\partial \mathbf{h}}{\partial \mathbf{x}} \) and \( \delta = \mathbf{x}-\mathbf{x}_0 \). The quadratic approximation yields normal equations:

\[ \left(\mathbf{P}^{-1} + \mathbf{H}^\top\mathbf{R}^{-1}\mathbf{H}\right)\,\delta = \mathbf{H}^\top\mathbf{R}^{-1}\left(\mathbf{y}-\mathbf{h}(\mathbf{x}_0)\right) + \mathbf{P}^{-1}\left(\mathbf{x}^{-}-\mathbf{x}_0\right). \]

Solving for \( \delta \) and updating \( \mathbf{x}_0 \leftarrow \mathbf{x}_0 + \delta \) is the core numerical mechanism used in many modern estimators (including graph-based SLAM, later).

4. Why Navigation Depends on Estimation Error (Practical Stability Bound)

Navigation computes commands from \( \hat{\mathbf{x}} \), not from the true \( \mathbf{x} \). Define estimation error \( \tilde{\mathbf{x}}_k = \mathbf{x}_k - \hat{\mathbf{x}}_k \). Even if the controller is stabilizing for perfect state knowledge, estimation error behaves like a disturbance.

flowchart TD
  X["True state x"] -->|"y = h(x) + v"| Y["Measurements y"]
  Y --> EST["Estimator"]
  EST --> XH["Estimate x_hat"]
  XH -->|"u = pi(x_hat)"| U["Control u"]
  U --> PLANT["Robot dynamics"]
  PLANT --> X
        

To make this precise without using later probabilistic machinery, consider a simplified kinematic model in which the planar position \( \mathbf{p} \in \mathbb{R}^2 \) evolves as: \( \mathbf{p}_{k+1} = \mathbf{p}_k + \Delta t\,\mathbf{v}_k \). Suppose we command a “go-to-goal” velocity based on the estimated position: \( \mathbf{v}_k = -k\,(\hat{\mathbf{p}}_k-\mathbf{p}_g) \), with gain \( k \gt 0 \) and goal \( \mathbf{p}_g \).

Let the true position error be \( \mathbf{e}_k = \mathbf{p}_k-\mathbf{p}_g \), and the position estimation error be \( \tilde{\mathbf{p}}_k = \mathbf{p}_k-\hat{\mathbf{p}}_k \). Substituting the control law gives:

\[ \mathbf{e}_{k+1} = \mathbf{e}_k - k\Delta t\,(\mathbf{e}_k - \tilde{\mathbf{p}}_k) = (1-k\Delta t)\,\mathbf{e}_k + k\Delta t\,\tilde{\mathbf{p}}_k. \]

Proposition (ultimate bound). If \( 0 \lt k\Delta t \lt 1 \) and \( \|\tilde{\mathbf{p}}_k\| \le \varepsilon \) for all \( k \), then the tracking error satisfies:

\[ \limsup_{k\to\infty} \|\mathbf{e}_k\| \le \varepsilon. \]

Proof. Let \( a = 1-k\Delta t \), so \( |a| \lt 1 \). Unrolling the recursion yields:

\[ \mathbf{e}_k = a^k\mathbf{e}_0 + k\Delta t\sum_{i=0}^{k-1} a^{k-1-i}\tilde{\mathbf{p}}_i. \]

Taking norms and applying the bound \( \|\tilde{\mathbf{p}}_i\| \le \varepsilon \) gives \( \|\mathbf{e}_k\| \le |a|^k\|\mathbf{e}_0\| + k\Delta t\,\varepsilon\sum_{j=0}^{k-1}|a|^j \). Since the geometric sum is bounded and the transient term vanishes, we obtain \( \limsup_{k\to\infty}\|\mathbf{e}_k\| \le \varepsilon \). \( \square \)

Interpretation: a controller can only drive the robot to within the estimator’s accuracy. This motivates the “pipeline mindset”: if sensing quality drops (occlusion, slip), estimation error grows, and navigation must either slow down, re-localize, or switch modes (Chapter 14 recovery behaviors).

5. Practical Modeling Assumptions in the Pipeline

For early modeling and simulation (this chapter), we typically commit to a set of assumptions that make the pipeline tractable. Each assumption can later be relaxed, but doing so has mathematical consequences.

  • Time synchronization: assume sensor timestamps can be aligned to the estimator time grid. Violations appear as effective noise/bias in \( \mathbf{v}_k \).
  • Calibration: assume known transforms between sensor frames and body frame. If extrinsics are wrong, the measurement function \( \mathbf{h} \) is misspecified.
  • Static or slowly varying world model: obstacles move slowly compared to reaction rate, or are handled by local sensing layers.
  • Separation of layers: estimation outputs a state; navigation consumes the state and a map. This is an architectural assumption, not a law of nature.
  • Bounded disturbances: for stability reasoning, assume \( \|\mathbf{w}_k\| \) and \( \|\mathbf{v}_k\| \) are bounded.

These assumptions are the “contract” between modules: sensing commits to providing measurements with known units and approximate error statistics; estimation commits to producing a best-effort state and a confidence measure; navigation commits to generating commands that remain safe under that confidence.

6. Python Lab — Minimal Pipeline Simulation with WLS Estimation

This lab implements the pipeline end-to-end in a self-contained 2D simulation: (i) a unicycle robot, (ii) noisy odometry, (iii) range-bearing measurements to known landmarks, (iv) an iterated weighted least-squares correction step, and (v) a simple go-to-goal controller. The estimator is written in optimization form (Section 3).

Chapter1_Lesson4.py


"""
Chapter1_Lesson4.py
Sensing–Estimation–Navigation pipeline demo (2D unicycle) using weighted least squares (WLS)
prior + range-bearing landmark measurements.
Dependencies: numpy, matplotlib
Optional robotics ecosystem pointers: ROS 2 (rclpy), nav2, GTSAM (not required for this demo).
"""

import numpy as np
import matplotlib.pyplot as plt

def wrap_angle(a: float) -> float:
    """Wrap angle to (-pi, pi]."""
    return (a + np.pi) % (2.0 * np.pi) - np.pi

def unicycle_step(x: np.ndarray, v: float, w: float, dt: float) -> np.ndarray:
    """Discrete-time unicycle integration."""
    x_new = x.copy()
    th = x[2]
    x_new[0] = x[0] + v * dt * np.cos(th)
    x_new[1] = x[1] + v * dt * np.sin(th)
    x_new[2] = wrap_angle(x[2] + w * dt)
    return x_new

def meas_model(x: np.ndarray, landmark: np.ndarray) -> np.ndarray:
    """Range-bearing measurement z = [r, b] to landmark in world coordinates."""
    dx = landmark[0] - x[0]
    dy = landmark[1] - x[1]
    r = np.hypot(dx, dy)
    b = wrap_angle(np.arctan2(dy, dx) - x[2])
    return np.array([r, b])

def meas_jacobian(x: np.ndarray, landmark: np.ndarray) -> np.ndarray:
    """Jacobian H = d h / d x for range-bearing measurement."""
    dx = landmark[0] - x[0]
    dy = landmark[1] - x[1]
    q = dx*dx + dy*dy
    r = np.sqrt(q)

    # Avoid division by zero
    eps = 1e-9
    q = max(q, eps)
    r = max(r, eps)

    # h1 = r = sqrt((lx-x)^2 + (ly-y)^2)
    dr_dx = -(dx) / r
    dr_dy = -(dy) / r
    dr_dth = 0.0

    # h2 = b = atan2(dy, dx) - th
    db_dx =  dy / q
    db_dy = -dx / q
    db_dth = -1.0

    return np.array([[dr_dx, dr_dy, dr_dth],
                     [db_dx, db_dy, db_dth]])

def wls_update_iterated(x_prior: np.ndarray, P_prior: np.ndarray,
                        z_list: list, landmarks: np.ndarray,
                        R: np.ndarray, iters: int = 2) -> tuple[np.ndarray, np.ndarray]:
    """
    Iterated WLS (Gauss-Newton) for a prior + stacked measurements.
    Minimizes:
        (x-x_prior)^T P_prior^{-1} (x-x_prior) + sum_i (z_i - h_i(x))^T R^{-1} (z_i - h_i(x))
    """
    if len(z_list) == 0:
        return x_prior, P_prior

    # Stack measurements
    z = np.concatenate(z_list, axis=0)  # shape (2m,)
    m = len(z_list)

    # Block-diagonal measurement covariance
    R_big = np.kron(np.eye(m), R)  # (2m x 2m)
    Rinv = np.linalg.inv(R_big)

    Pinv = np.linalg.inv(P_prior)

    x = x_prior.copy()
    for _ in range(iters):
        # Build stacked residual and Jacobian
        h = []
        H = []
        for i in range(m):
            hi = meas_model(x, landmarks[i])
            h.append(hi)
            H.append(meas_jacobian(x, landmarks[i]))
        h = np.concatenate(h, axis=0)
        H = np.vstack(H)

        # Residual (bearing residual must be wrapped)
        res = z - h
        for i in range(m):
            res[2*i + 1] = wrap_angle(res[2*i + 1])

        # Solve normal equations for delta:
        # (P^{-1} + H^T R^{-1} H) delta = H^T R^{-1} res + P^{-1}(x_prior - x)
        A = Pinv + H.T @ Rinv @ H
        b = H.T @ Rinv @ res + Pinv @ (x_prior - x)
        delta = np.linalg.solve(A, b)

        x = x + delta
        x[2] = wrap_angle(x[2])

    # Posterior covariance at final linearization point
    h = []
    H = []
    for i in range(m):
        h.append(meas_model(x, landmarks[i]))
        H.append(meas_jacobian(x, landmarks[i]))
    H = np.vstack(H)
    P_post = np.linalg.inv(Pinv + H.T @ Rinv @ H)
    return x, P_post

def main():
    np.random.seed(7)

    # Map (known landmarks): choose 3 well-spread landmarks
    landmarks = np.array([[5.0, 0.0],
                          [6.0, 6.0],
                          [0.0, 6.0]])

    dt = 0.1
    N = 350

    # True state and estimate (x, y, theta)
    x_true = np.array([0.0, 0.0, 0.0])
    x_hat  = np.array([0.0, 0.0, 0.0])

    # Prior covariance
    P = np.diag([0.15**2, 0.15**2, (np.deg2rad(8.0))**2])

    # Noise levels
    sigma_v = 0.08          # m/s odometry noise
    sigma_w = np.deg2rad(3) # rad/s odometry noise
    sigma_r = 0.12          # m range noise
    sigma_b = np.deg2rad(2) # rad bearing noise

    # Navigation goal and controller gains (simple go-to-goal)
    goal = np.array([7.0, 7.0])
    k_heading = 1.6
    v_max = 0.9

    # Measurement covariance per landmark measurement
    R = np.diag([sigma_r**2, sigma_b**2])

    hist_true = np.zeros((N, 3))
    hist_hat  = np.zeros((N, 3))

    for k in range(N):
        # --- Navigation (policy uses estimate) ---
        dx = goal[0] - x_hat[0]
        dy = goal[1] - x_hat[1]
        dist = np.hypot(dx, dy)
        desired_heading = np.arctan2(dy, dx)
        heading_error = wrap_angle(desired_heading - x_hat[2])

        v_cmd = v_max * np.tanh(dist)      # smoothly saturating speed
        w_cmd = k_heading * heading_error  # proportional heading control

        # --- True motion (includes unmodeled disturbances) ---
        # Model a mild lateral slip disturbance as small random heading perturbation.
        slip = np.random.normal(0.0, np.deg2rad(0.35))
        x_true = unicycle_step(x_true, v_cmd, w_cmd + slip/dt, dt)

        # --- Sensing: odometry (control-derived) ---
        v_meas = v_cmd + np.random.normal(0.0, sigma_v)
        w_meas = w_cmd + np.random.normal(0.0, sigma_w)

        # --- Estimation: prediction from odometry ---
        x_prior = unicycle_step(x_hat, v_meas, w_meas, dt)

        th = x_hat[2]
        F = np.array([[1.0, 0.0, -v_meas*dt*np.sin(th)],
                      [0.0, 1.0,  v_meas*dt*np.cos(th)],
                      [0.0, 0.0,  1.0]])

        # Simple process noise model (odometry-induced)
        Q = np.diag([(sigma_v*dt)**2, (sigma_v*dt)**2, (sigma_w*dt)**2])
        P_prior = F @ P @ F.T + Q

        # --- Sensing: landmark measurements (range-bearing) ---
        z_list = []
        # Use all landmarks within range to emulate a perception module
        max_range = 8.0
        for lm in landmarks:
            z_true = meas_model(x_true, lm)
            if z_true[0] <= max_range:
                z_noisy = z_true + np.array([np.random.normal(0.0, sigma_r),
                                             np.random.normal(0.0, sigma_b)])
                z_noisy[1] = wrap_angle(z_noisy[1])
                z_list.append(z_noisy)

        # --- Estimation: iterated WLS correction (prior + measurements) ---
        x_hat, P = wls_update_iterated(x_prior, P_prior, z_list, landmarks[:len(z_list)], R, iters=2)

        hist_true[k] = x_true
        hist_hat[k]  = x_hat

    # --- Visualization ---
    plt.figure()
    plt.plot(hist_true[:,0], hist_true[:,1], label="true")
    plt.plot(hist_hat[:,0],  hist_hat[:,1],  label="estimated")
    plt.scatter(landmarks[:,0], landmarks[:,1], marker="x", label="landmarks")
    plt.scatter(goal[0], goal[1], marker="*", label="goal")
    plt.axis("equal")
    plt.grid(True)
    plt.legend()
    plt.title("Sensing–Estimation–Navigation loop (demo)")
    plt.show()

if __name__ == "__main__":
    main()
      

Robotics libraries to know (Python): ROS 2 (rclpy) for messaging/execution, tf2 for frame transforms, and factor-graph tools such as GTSAM for optimization-based estimation. We keep this lab ROS-free so it runs in a standard scientific Python stack.

7. C++ Lab — Same Pipeline with Eigen and CSV Output

C++ is common for real-time robot stacks (ROS 2 nodes, high-rate perception). This implementation mirrors the Python estimator and writes a CSV trajectory for inspection.

Chapter1_Lesson4.cpp


// Chapter1_Lesson4.cpp
// Sensing–Estimation–Navigation pipeline demo (2D unicycle) using prior + WLS correction.
// Dependencies: C++17. Optional: Eigen (header-only) for linear algebra.
// Build example (with Eigen in include path):
//   g++ -O2 -std=c++17 Chapter1_Lesson4.cpp -I /usr/include/eigen3 -o amr_lesson4
//
// Output: writes a CSV file "traj_ch1_l4.csv" with true and estimated pose per step.
// Typical robotics ecosystem pointers: ROS 2 (rclcpp), Eigen, GTSAM (not required for this demo).

#include <Eigen/Dense>
#include <cmath>
#include <fstream>
#include <iostream>
#include <random>
#include <vector>

static double wrapAngle(double a) {
    const double TWO_PI = 2.0 * M_PI;
    a = std::fmod(a + M_PI, TWO_PI);
    if (a < 0.0) a += TWO_PI;
    return a - M_PI;
}

static Eigen::Vector3d unicycleStep(const Eigen::Vector3d& x, double v, double w, double dt) {
    Eigen::Vector3d xn = x;
    double th = x(2);
    xn(0) = x(0) + v * dt * std::cos(th);
    xn(1) = x(1) + v * dt * std::sin(th);
    xn(2) = wrapAngle(x(2) + w * dt);
    return xn;
}

static Eigen::Vector2d measModel(const Eigen::Vector3d& x, const Eigen::Vector2d& lm) {
    double dx = lm(0) - x(0);
    double dy = lm(1) - x(1);
    double r  = std::hypot(dx, dy);
    double b  = wrapAngle(std::atan2(dy, dx) - x(2));
    return Eigen::Vector2d(r, b);
}

static Eigen::Matrix<double,2,3> measJacobian(const Eigen::Vector3d& x, const Eigen::Vector2d& lm) {
    double dx = lm(0) - x(0);
    double dy = lm(1) - x(1);
    double q = dx*dx + dy*dy;
    double r = std::sqrt(std::max(q, 1e-12));

    q = std::max(q, 1e-12);

    Eigen::Matrix<double,2,3> H;
    // range derivatives
    H(0,0) = -dx / r;
    H(0,1) = -dy / r;
    H(0,2) = 0.0;
    // bearing derivatives
    H(1,0) =  dy / q;
    H(1,1) = -dx / q;
    H(1,2) = -1.0;
    return H;
}

static void wlsUpdateIterated(
    const Eigen::Vector3d& x_prior, const Eigen::Matrix3d& P_prior,
    const std::vector<Eigen::Vector2d>& z_list, const std::vector<Eigen::Vector2d>& lm_list,
    const Eigen::Matrix2d& R, int iters,
    Eigen::Vector3d& x_post, Eigen::Matrix3d& P_post
) {
    if (z_list.empty()) {
        x_post = x_prior;
        P_post = P_prior;
        return;
    }

    int m = static_cast<int>(z_list.size());
    Eigen::VectorXd z(2*m);
    for (int i = 0; i < m; ++i) {
        z.segment<2>(2*i) = z_list[i];
    }

    Eigen::MatrixXd Rbig = Eigen::MatrixXd::Zero(2*m, 2*m);
    for (int i = 0; i < m; ++i) {
        Rbig.block<2,2>(2*i, 2*i) = R;
    }
    Eigen::MatrixXd Rinv = Rbig.inverse();
    Eigen::Matrix3d Pinv = P_prior.inverse();

    Eigen::Vector3d x = x_prior;

    for (int it = 0; it < iters; ++it) {
        Eigen::VectorXd h(2*m);
        Eigen::MatrixXd H(2*m, 3);

        for (int i = 0; i < m; ++i) {
            Eigen::Vector2d hi = measModel(x, lm_list[i]);
            h.segment<2>(2*i) = hi;
            H.block<2,3>(2*i, 0) = measJacobian(x, lm_list[i]);
        }

        Eigen::VectorXd res = z - h;
        for (int i = 0; i < m; ++i) {
            res(2*i + 1) = wrapAngle(res(2*i + 1));
        }

        Eigen::Matrix3d A = Pinv + H.transpose() * Rinv * H;
        Eigen::Vector3d b = H.transpose() * Rinv * res + Pinv * (x_prior - x);

        Eigen::Vector3d delta = A.ldlt().solve(b);
        x = x + delta;
        x(2) = wrapAngle(x(2));
    }

    // Final covariance
    Eigen::MatrixXd H(2*m, 3);
    for (int i = 0; i < m; ++i) {
        H.block<2,3>(2*i, 0) = measJacobian(x, lm_list[i]);
    }
    Eigen::Matrix3d Pinv_post = Pinv + H.transpose() * Rinv * H;
    P_post = Pinv_post.inverse();
    x_post = x;
}

int main() {
    std::mt19937 rng(7);
    std::normal_distribution<double> n01(0.0, 1.0);

    // Landmarks (known map)
    std::vector<Eigen::Vector2d> landmarks;
    landmarks.emplace_back(5.0, 0.0);
    landmarks.emplace_back(6.0, 6.0);
    landmarks.emplace_back(0.0, 6.0);

    const double dt = 0.1;
    const int N = 350;

    Eigen::Vector3d x_true(0.0, 0.0, 0.0);
    Eigen::Vector3d x_hat(0.0, 0.0, 0.0);

    Eigen::Matrix3d P = Eigen::Matrix3d::Zero();
    P(0,0) = 0.15*0.15;
    P(1,1) = 0.15*0.15;
    P(2,2) = std::pow(8.0*M_PI/180.0, 2);

    const double sigma_v = 0.08;
    const double sigma_w = 3.0*M_PI/180.0;
    const double sigma_r = 0.12;
    const double sigma_b = 2.0*M_PI/180.0;

    Eigen::Vector2d goal(7.0, 7.0);
    const double k_heading = 1.6;
    const double v_max = 0.9;

    Eigen::Matrix2d R = Eigen::Matrix2d::Zero();
    R(0,0) = sigma_r*sigma_r;
    R(1,1) = sigma_b*sigma_b;

    std::ofstream csv("traj_ch1_l4.csv");
    csv << "k,x_true,y_true,th_true,x_hat,y_hat,th_hat\n";

    for (int k = 0; k < N; ++k) {
        // Navigation uses estimate
        double dx = goal(0) - x_hat(0);
        double dy = goal(1) - x_hat(1);
        double dist = std::hypot(dx, dy);
        double desired = std::atan2(dy, dx);
        double heading_err = wrapAngle(desired - x_hat(2));

        double v_cmd = v_max * std::tanh(dist);
        double w_cmd = k_heading * heading_err;

        // True motion includes mild slip
        double slip = (0.35*M_PI/180.0) * n01(rng);
        x_true = unicycleStep(x_true, v_cmd, w_cmd + slip/dt, dt);

        // Odometry
        double v_meas = v_cmd + sigma_v * n01(rng);
        double w_meas = w_cmd + sigma_w * n01(rng);

        Eigen::Vector3d x_prior = unicycleStep(x_hat, v_meas, w_meas, dt);

        double th = x_hat(2);
        Eigen::Matrix3d F = Eigen::Matrix3d::Identity();
        F(0,2) = -v_meas*dt*std::sin(th);
        F(1,2) =  v_meas*dt*std::cos(th);

        Eigen::Matrix3d Q = Eigen::Matrix3d::Zero();
        Q(0,0) = std::pow(sigma_v*dt, 2);
        Q(1,1) = std::pow(sigma_v*dt, 2);
        Q(2,2) = std::pow(sigma_w*dt, 2);

        Eigen::Matrix3d P_prior = F * P * F.transpose() + Q;

        // Landmark measurements
        std::vector<Eigen::Vector2d> z_list;
        std::vector<Eigen::Vector2d> lm_list;
        const double max_range = 8.0;

        for (const auto& lm : landmarks) {
            Eigen::Vector2d z_true = measModel(x_true, lm);
            if (z_true(0) <= max_range) {
                Eigen::Vector2d z_noisy = z_true;
                z_noisy(0) += sigma_r * n01(rng);
                z_noisy(1)  = wrapAngle(z_noisy(1) + sigma_b * n01(rng));
                z_list.push_back(z_noisy);
                lm_list.push_back(lm);
            }
        }

        // WLS correction
        Eigen::Vector3d x_post;
        Eigen::Matrix3d P_post;
        wlsUpdateIterated(x_prior, P_prior, z_list, lm_list, R, 2, x_post, P_post);

        x_hat = x_post;
        P = P_post;

        csv << k << "," << x_true(0) << "," << x_true(1) << "," << x_true(2) << ","
            << x_hat(0)  << "," << x_hat(1)  << "," << x_hat(2)  << "\n";
    }

    std::cout << "Wrote traj_ch1_l4.csv\n";
    return 0;
}
      

Robotics libraries to know (C++): ROS 2 (rclcpp), Eigen, and optimization/SLAM libraries such as Ceres Solver and GTSAM. Eigen is used here for compact linear algebra.

8. Java Lab — Self-Contained Pipeline and CSV Output

Java is less common on embedded robot controllers but appears in enterprise AMR systems and simulation tooling. This version is self-contained and avoids external dependencies by coding small matrix routines directly.

Chapter1_Lesson4.java


// Chapter1_Lesson4.java
// Sensing–Estimation–Navigation pipeline demo (2D unicycle) using prior + WLS correction.
// Build:
//   javac Chapter1_Lesson4.java
// Run:
//   java Chapter1_Lesson4
//
// Output: "traj_ch1_l4_java.csv" with true and estimated pose.
// Robotics ecosystem pointers: ROSJava (rosjava), EJML for matrices, JVX? (not required here).
// This demo uses small hand-coded linear algebra to stay self-contained.

import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;

public class Chapter1_Lesson4 {

    static double wrapAngle(double a) {
        double twoPi = 2.0 * Math.PI;
        a = (a + Math.PI) % twoPi;
        if (a < 0.0) a += twoPi;
        return a - Math.PI;
    }

    static double[] unicycleStep(double[] x, double v, double w, double dt) {
        double[] xn = x.clone();
        double th = x[2];
        xn[0] = x[0] + v * dt * Math.cos(th);
        xn[1] = x[1] + v * dt * Math.sin(th);
        xn[2] = wrapAngle(x[2] + w * dt);
        return xn;
    }

    static double[] measModel(double[] x, double[] lm) {
        double dx = lm[0] - x[0];
        double dy = lm[1] - x[1];
        double r = Math.hypot(dx, dy);
        double b = wrapAngle(Math.atan2(dy, dx) - x[2]);
        return new double[]{r, b};
    }

    static double[][] measJacobian(double[] x, double[] lm) {
        double dx = lm[0] - x[0];
        double dy = lm[1] - x[1];
        double q = dx*dx + dy*dy;
        q = Math.max(q, 1e-12);
        double r = Math.sqrt(q);
        r = Math.max(r, 1e-12);

        double[][] H = new double[2][3];
        // range
        H[0][0] = -dx / r;
        H[0][1] = -dy / r;
        H[0][2] = 0.0;
        // bearing
        H[1][0] =  dy / q;
        H[1][1] = -dx / q;
        H[1][2] = -1.0;
        return H;
    }

    // Small 3x3 linear algebra helpers
    static double[][] matAdd3(double[][] A, double[][] B) {
        double[][] C = new double[3][3];
        for (int i=0;i<3;i++) for (int j=0;j<3;j++) C[i][j] = A[i][j] + B[i][j];
        return C;
    }
    static double[][] matMul3(double[][] A, double[][] B) {
        double[][] C = new double[3][3];
        for (int i=0;i<3;i++) for (int j=0;j<3;j++) {
            double s=0.0;
            for (int k=0;k<3;k++) s += A[i][k]*B[k][j];
            C[i][j]=s;
        }
        return C;
    }
    static double[] matVec3(double[][] A, double[] x) {
        double[] y = new double[3];
        for (int i=0;i<3;i++) {
            y[i]=A[i][0]*x[0] + A[i][1]*x[1] + A[i][2]*x[2];
        }
        return y;
    }
    static double[][] transpose3(double[][] A) {
        double[][] T = new double[3][3];
        for (int i=0;i<3;i++) for (int j=0;j<3;j++) T[i][j] = A[j][i];
        return T;
    }

    // Inverse of 3x3 matrix via adjugate (sufficient for this small demo)
    static double[][] inv3(double[][] A) {
        double a=A[0][0], b=A[0][1], c=A[0][2];
        double d=A[1][0], e=A[1][1], f=A[1][2];
        double g=A[2][0], h=A[2][1], i=A[2][2];
        double det = a*(e*i - f*h) - b*(d*i - f*g) + c*(d*h - e*g);
        if (Math.abs(det) < 1e-12) det = (det >= 0 ? 1e-12 : -1e-12);

        double[][] inv = new double[3][3];
        inv[0][0] =  (e*i - f*h)/det;
        inv[0][1] = -(b*i - c*h)/det;
        inv[0][2] =  (b*f - c*e)/det;
        inv[1][0] = -(d*i - f*g)/det;
        inv[1][1] =  (a*i - c*g)/det;
        inv[1][2] = -(a*f - c*d)/det;
        inv[2][0] =  (d*h - e*g)/det;
        inv[2][1] = -(a*h - b*g)/det;
        inv[2][2] =  (a*e - b*d)/det;
        return inv;
    }

    // Solve 3x3 system A x = b (Gaussian elimination)
    static double[] solve3(double[][] A, double[] b) {
        double[][] M = new double[3][4];
        for (int r=0;r<3;r++) {
            System.arraycopy(A[r], 0, M[r], 0, 3);
            M[r][3] = b[r];
        }
        for (int col=0; col<3; col++) {
            // pivot
            int piv = col;
            for (int r=col+1;r<3;r++) if (Math.abs(M[r][col]) > Math.abs(M[piv][col])) piv = r;
            double[] tmp = M[col]; M[col] = M[piv]; M[piv] = tmp;

            double pivot = M[col][col];
            if (Math.abs(pivot) < 1e-12) pivot = (pivot>=0 ? 1e-12 : -1e-12);
            for (int c=col;c<4;c++) M[col][c] /= pivot;

            for (int r=0;r<3;r++) if (r != col) {
                double factor = M[r][col];
                for (int c=col;c<4;c++) M[r][c] -= factor * M[col][c];
            }
        }
        return new double[]{M[0][3], M[1][3], M[2][3]};
    }

    static void wlsUpdateIterated(
            double[] xPrior, double[][] PPrior,
            double[][] zList, double[][] lmList, double[][] R, int iters,
            double[] xPost, double[][] PPost) {

        int m = zList.length;
        if (m == 0) {
            System.arraycopy(xPrior, 0, xPost, 0, 3);
            for (int r=0;r<3;r++) System.arraycopy(PPrior[r], 0, PPost[r], 0, 3);
            return;
        }

        // Build R^{-1} for each measurement (2x2)
        double detR = R[0][0]*R[1][1] - R[0][1]*R[1][0];
        double[][] Rinv = new double[][] {
                { R[1][1]/detR, -R[0][1]/detR},
                {-R[1][0]/detR,  R[0][0]/detR}
        };

        double[][] Pinv = inv3(PPrior);
        double[] x = xPrior.clone();

        for (int it=0; it<iters; it++) {
            // Accumulate normal equation pieces:
            // A = Pinv + sum H^T Rinv H,  b = sum H^T Rinv res + Pinv(xPrior - x)
            double[][] A = new double[3][3];
            for (int r=0;r<3;r++) System.arraycopy(Pinv[r], 0, A[r], 0, 3);

            double[] b = matVec3(Pinv, new double[]{xPrior[0]-x[0], xPrior[1]-x[1], wrapAngle(xPrior[2]-x[2])});

            for (int j=0; j<m; j++) {
                double[] z = zList[j];
                double[] lm = lmList[j];

                double[] h = measModel(x, lm);
                double[] res = new double[]{z[0]-h[0], wrapAngle(z[1]-h[1])};

                double[][] H = measJacobian(x, lm); // 2x3

                // Compute H^T Rinv H (3x3) and H^T Rinv res (3)
                double[][] RtH = new double[2][3];
                for (int r=0;r<2;r++) for (int c=0;c<3;c++) {
                    RtH[r][c] = Rinv[r][0]*H[0][c] + Rinv[r][1]*H[1][c];
                }
                // add to A: H^T * (Rinv*H)
                for (int r=0;r<3;r++) for (int c=0;c<3;c++) {
                    A[r][c] += H[0][r]*RtH[0][c] + H[1][r]*RtH[1][c];
                }

                // add to b: H^T * (Rinv*res)
                double r0 = Rinv[0][0]*res[0] + Rinv[0][1]*res[1];
                double r1 = Rinv[1][0]*res[0] + Rinv[1][1]*res[1];
                b[0] += H[0][0]*r0 + H[1][0]*r1;
                b[1] += H[0][1]*r0 + H[1][1]*r1;
                b[2] += H[0][2]*r0 + H[1][2]*r1;
            }

            double[] delta = solve3(A, b);
            x[0] += delta[0];
            x[1] += delta[1];
            x[2] = wrapAngle(x[2] + delta[2]);
        }

        // Approximate posterior covariance by recomputing A at final x
        double[][] A = new double[3][3];
        for (int r=0;r<3;r++) System.arraycopy(Pinv[r], 0, A[r], 0, 3);
        for (int j=0; j<m; j++) {
            double[][] H = measJacobian(x, lmList[j]);
            // A += H^T Rinv H
            double[][] RtH = new double[2][3];
            for (int r=0;r<2;r++) for (int c=0;c<3;c++) {
                RtH[r][c] = Rinv[r][0]*H[0][c] + Rinv[r][1]*H[1][c];
            }
            for (int r=0;r<3;r++) for (int c=0;c<3;c++) {
                A[r][c] += H[0][r]*RtH[0][c] + H[1][r]*RtH[1][c];
            }
        }
        double[][] P = inv3(A);

        System.arraycopy(x, 0, xPost, 0, 3);
        for (int r=0;r<3;r++) System.arraycopy(P[r], 0, PPost[r], 0, 3);
    }

    public static void main(String[] args) throws IOException {
        Random rng = new Random(7);

        double[][] landmarks = new double[][] {
                {5.0, 0.0},
                {6.0, 6.0},
                {0.0, 6.0}
        };

        double dt = 0.1;
        int N = 350;

        double[] xTrue = new double[]{0.0, 0.0, 0.0};
        double[] xHat  = new double[]{0.0, 0.0, 0.0};

        double[][] P = new double[][] {
                {0.15*0.15, 0.0, 0.0},
                {0.0, 0.15*0.15, 0.0},
                {0.0, 0.0, Math.pow(Math.toRadians(8.0), 2)}
        };

        double sigmaV = 0.08;
        double sigmaW = Math.toRadians(3.0);
        double sigmaR = 0.12;
        double sigmaB = Math.toRadians(2.0);

        double[] goal = new double[]{7.0, 7.0};
        double kHeading = 1.6;
        double vMax = 0.9;

        double[][] R = new double[][] {
                {sigmaR*sigmaR, 0.0},
                {0.0, sigmaB*sigmaB}
        };

        FileWriter csv = new FileWriter("traj_ch1_l4_java.csv");
        csv.write("k,x_true,y_true,th_true,x_hat,y_hat,th_hat\n");

        for (int k=0; k<N; k++) {
            // Navigation uses estimate
            double dx = goal[0] - xHat[0];
            double dy = goal[1] - xHat[1];
            double dist = Math.hypot(dx, dy);
            double desired = Math.atan2(dy, dx);
            double headingErr = wrapAngle(desired - xHat[2]);

            double vCmd = vMax * Math.tanh(dist);
            double wCmd = kHeading * headingErr;

            // True motion includes mild slip
            double slip = Math.toRadians(0.35) * rng.nextGaussian();
            xTrue = unicycleStep(xTrue, vCmd, wCmd + slip/dt, dt);

            // Odometry
            double vMeas = vCmd + sigmaV * rng.nextGaussian();
            double wMeas = wCmd + sigmaW * rng.nextGaussian();

            double[] xPrior = unicycleStep(xHat, vMeas, wMeas, dt);

            double th = xHat[2];
            double[][] F = new double[][] {
                    {1.0, 0.0, -vMeas*dt*Math.sin(th)},
                    {0.0, 1.0,  vMeas*dt*Math.cos(th)},
                    {0.0, 0.0,  1.0}
            };

            double[][] Q = new double[][] {
                    {Math.pow(sigmaV*dt,2), 0.0, 0.0},
                    {0.0, Math.pow(sigmaV*dt,2), 0.0},
                    {0.0, 0.0, Math.pow(sigmaW*dt,2)}
            };

            double[][] FP = matMul3(F, P);
            double[][] FPFT = matMul3(FP, transpose3(F));
            double[][] PPrior = matAdd3(FPFT, Q);

            // Measurements
            double maxRange = 8.0;
            int count = 0;
            double[][] zListTemp = new double[landmarks.length][2];
            double[][] lmListTemp = new double[landmarks.length][2];
            for (int j=0; j<landmarks.length; j++) {
                double[] zTrue = measModel(xTrue, landmarks[j]);
                if (zTrue[0] <= maxRange) {
                    double[] zNoisy = new double[] {
                            zTrue[0] + sigmaR * rng.nextGaussian(),
                            wrapAngle(zTrue[1] + sigmaB * rng.nextGaussian())
                    };
                    zListTemp[count] = zNoisy;
                    lmListTemp[count] = landmarks[j];
                    count++;
                }
            }
            double[][] zList = new double[count][2];
            double[][] lmList = new double[count][2];
            for (int j=0; j<count; j++) {
                zList[j] = zListTemp[j];
                lmList[j] = lmListTemp[j];
            }

            double[] xPost = new double[3];
            double[][] PPost = new double[3][3];
            wlsUpdateIterated(xPrior, PPrior, zList, lmList, R, 2, xPost, PPost);

            xHat = xPost;
            P = PPost;

            csv.write(String.format("%d,%.6f,%.6f,%.6f,%.6f,%.6f,%.6f\n",
                    k, xTrue[0], xTrue[1], xTrue[2], xHat[0], xHat[1], xHat[2]));
        }

        csv.close();
        System.out.println("Wrote traj_ch1_l4_java.csv");
    }
}
      

Robotics libraries to know (Java): EJML for matrices, ROSJava for ROS ecosystems, and simulation frameworks that integrate Java components. In later chapters, using EJML would simplify the estimator code.

9. MATLAB + Simulink Lab — Script Simulation and Model Builder

MATLAB is widely used for control design and rapid prototyping. The first script mirrors the Python simulation. The second script programmatically builds a Simulink model of the pipeline (useful for block-diagram workflows).

Chapter1_Lesson4.m


% Chapter1_Lesson4.m
% Sensing–Estimation–Navigation pipeline demo (2D unicycle) using WLS (prior + range-bearing).
% Requires: base MATLAB. Optional (not required): Robotics System Toolbox / Simulink for later chapters.
% Run:
%   Chapter1_Lesson4

clear; clc; close all;
rng(7);

wrapAngle = @(a) mod(a + pi, 2*pi) - pi;

unicycleStep = @(x,v,w,dt) [ ...
    x(1) + v*dt*cos(x(3)); ...
    x(2) + v*dt*sin(x(3)); ...
    wrapAngle(x(3) + w*dt) ];

measModel = @(x,lm) [ ...
    hypot(lm(1)-x(1), lm(2)-x(2)); ...
    wrapAngle(atan2(lm(2)-x(2), lm(1)-x(1)) - x(3)) ];

measJacobian = @(x,lm) localMeasJacobian(x,lm);

% Map (known landmarks)
landmarks = [5 0; 6 6; 0 6];

dt = 0.1; N = 350;

xTrue = [0;0;0];
xHat  = [0;0;0];

P = diag([0.15^2, 0.15^2, deg2rad(8)^2]);

sigmaV = 0.08;
sigmaW = deg2rad(3);
sigmaR = 0.12;
sigmaB = deg2rad(2);

goal = [7;7];
kHeading = 1.6;
vMax = 0.9;

R = diag([sigmaR^2, sigmaB^2]);

histTrue = zeros(N,3);
histHat  = zeros(N,3);

for k=1:N
    % --- Navigation (policy uses estimate) ---
    dx = goal(1) - xHat(1);
    dy = goal(2) - xHat(2);
    dist = hypot(dx,dy);
    desired = atan2(dy,dx);
    headingErr = wrapAngle(desired - xHat(3));
    vCmd = vMax*tanh(dist);
    wCmd = kHeading*headingErr;

    % --- True motion with mild slip disturbance ---
    slip = deg2rad(0.35)*randn;
    xTrue = unicycleStep(xTrue, vCmd, wCmd + slip/dt, dt);

    % --- Odometry ---
    vMeas = vCmd + sigmaV*randn;
    wMeas = wCmd + sigmaW*randn;

    % --- Prediction ---
    xPrior = unicycleStep(xHat, vMeas, wMeas, dt);

    th = xHat(3);
    F = [1 0 -vMeas*dt*sin(th);
         0 1  vMeas*dt*cos(th);
         0 0  1];

    Q = diag([(sigmaV*dt)^2, (sigmaV*dt)^2, (sigmaW*dt)^2]);
    PPrior = F*P*F' + Q;

    % --- Landmark measurements ---
    zList = [];
    lmList = [];
    maxRange = 8.0;
    for j=1:size(landmarks,1)
        zTrue = measModel(xTrue, landmarks(j,:)');
        if zTrue(1) <= maxRange
            zNoisy = zTrue + [sigmaR*randn; sigmaB*randn];
            zNoisy(2) = wrapAngle(zNoisy(2));
            zList = [zList; zNoisy];
            lmList = [lmList; landmarks(j,:)];
        end
    end

    % --- Iterated WLS correction (2 iterations) ---
    [xHat, P] = wlsUpdateIterated(xPrior, PPrior, zList, lmList, R, 2, wrapAngle, measModel, measJacobian);

    histTrue(k,:) = xTrue';
    histHat(k,:)  = xHat';
end

figure;
plot(histTrue(:,1), histTrue(:,2), 'LineWidth', 1.5); hold on;
plot(histHat(:,1), histHat(:,2),  'LineWidth', 1.5);
scatter(landmarks(:,1), landmarks(:,2), 70, 'x', 'LineWidth', 2);
scatter(goal(1), goal(2), 90, '*', 'LineWidth', 2);
axis equal; grid on;
legend('true','estimated','landmarks','goal');
title('Sensing–Estimation–Navigation loop (demo)');

% ===== Local functions =====

function H = localMeasJacobian(x,lm)
    dx = lm(1) - x(1);
    dy = lm(2) - x(2);
    q = dx^2 + dy^2;
    q = max(q, 1e-12);
    r = sqrt(q);
    r = max(r, 1e-12);

    H = zeros(2,3);
    H(1,1) = -dx/r;
    H(1,2) = -dy/r;
    H(1,3) = 0;

    H(2,1) =  dy/q;
    H(2,2) = -dx/q;
    H(2,3) = -1;
end

function [xPost, PPost] = wlsUpdateIterated(xPrior, PPrior, zList, lmList, R, iters, wrapAngle, measModel, measJacobian)
    if isempty(zList)
        xPost = xPrior;
        PPost = PPrior;
        return;
    end

    m = size(zList,1);
    z = reshape(zList', [], 1); % (2m x 1)

    Rbig = kron(eye(m), R);
    Rinv = inv(Rbig);
    Pinv = inv(PPrior);

    x = xPrior;
    for it = 1:iters
        h = zeros(2*m,1);
        H = zeros(2*m,3);
        for j=1:m
            lm = lmList(j,:)';
            hj = measModel(x,lm);
            h(2*j-1:2*j) = hj;
            H(2*j-1:2*j,:) = measJacobian(x,lm);
        end

        res = z - h;
        for j=1:m
            res(2*j) = wrapAngle(res(2*j));
        end

        A = Pinv + H'*Rinv*H;
        b = H'*Rinv*res + Pinv*(xPrior - x);

        delta = A \ b;
        x = x + delta;
        x(3) = wrapAngle(x(3));
    end

    % Posterior covariance at final linearization point
    H = zeros(2*m,3);
    for j=1:m
        lm = lmList(j,:)';
        H(2*j-1:2*j,:) = measJacobian(x,lm);
    end
    PPost = inv(Pinv + H'*Rinv*H);
    xPost = x;
end
      

Chapter1_Lesson4_Simulink.m


% Chapter1_Lesson4_Simulink.m
% Programmatically builds a Simulink model that mirrors the sensing–estimation–navigation pipeline.
%
% Requires: Simulink. Optional: Robotics System Toolbox (not required).
%
% What you get:
%   Model name: ch1_l4_pipeline.slx
%   Blocks:
%     - Controller (MATLAB Function): go-to-goal based on x_hat
%     - Plant (MATLAB Function): unicycle step producing x_true
%     - Odometry noise and integration (MATLAB Function): produces x_prior
%     - Landmark sensor (MATLAB Function): range-bearing z
%     - Estimator (MATLAB Function): one Gauss-Newton/WLS step -> x_hat
%
% Run:
%   Chapter1_Lesson4_Simulink
%   open_system('ch1_l4_pipeline')

clear; clc;

model = 'ch1_l4_pipeline';
if bdIsLoaded(model)
    close_system(model, 0);
end
new_system(model);
open_system(model);

set_param(model, 'StopTime', '35');      % 35 s
set_param(model, 'FixedStep', '0.1');    % dt = 0.1
set_param(model, 'Solver', 'FixedStepDiscrete');

% Layout helpers
x0 = 50; y0 = 60; dx = 220; dy = 110;
blkW = 150; blkH = 60;

% --- Source: goal (constant) ---
add_block('simulink/Sources/Constant', [model '/Goal'], ...
    'Position', [x0 y0 x0+blkW y0+blkH], ...
    'Value', '[7;7]');

% --- Initial x_hat (Unit Delay) ---
add_block('simulink/Discrete/Unit Delay', [model '/x_hat(z^-1)'], ...
    'Position', [x0 y0+dy x0+blkW y0+dy+blkH], ...
    'InitialCondition', '[0;0;0]');

% --- Controller MATLAB Function ---
add_block('simulink/User-Defined Functions/MATLAB Function', [model '/Controller'], ...
    'Position', [x0+dx y0+dy x0+dx+blkW y0+dy+blkH]);

set_param([model '/Controller'], 'Script', sprintf([ ...
'function u = f(goal, xhat)\n' ...
'%% go-to-goal controller using estimate\n' ...
'%% xhat = [x;y;th], u = [v;w]\n' ...
'kHeading = 1.6; vMax = 0.9;\n' ...
'dx = goal(1) - xhat(1);\n' ...
'dy = goal(2) - xhat(2);\n' ...
'dist = hypot(dx,dy);\n' ...
'desired = atan2(dy,dx);\n' ...
'headingErr = wrapToPi(desired - xhat(3));\n' ...
'v = vMax * tanh(dist);\n' ...
'w = kHeading * headingErr;\n' ...
'u = [v;w];\n' ...
'end\n' ...
])));

% --- Plant MATLAB Function (true dynamics) ---
add_block('simulink/User-Defined Functions/MATLAB Function', [model '/Plant'], ...
    'Position', [x0+2*dx y0+dy x0+2*dx+blkW y0+dy+blkH]);

set_param([model '/Plant'], 'Script', sprintf([ ...
'function xtrue = f(xtrue_prev, u)\n' ...
'%% unicycle discrete dynamics, dt = 0.1 assumed\n' ...
'dt = 0.1;\n' ...
'v = u(1); w = u(2);\n' ...
'th = xtrue_prev(3);\n' ...
'x = xtrue_prev(1) + v*dt*cos(th);\n' ...
'y = xtrue_prev(2) + v*dt*sin(th);\n' ...
'th = wrapToPi(xtrue_prev(3) + w*dt);\n' ...
'xtrue = [x;y;th];\n' ...
'end\n' ...
])));

% --- True state memory (Unit Delay) ---
add_block('simulink/Discrete/Unit Delay', [model '/x_true(z^-1)'], ...
    'Position', [x0+2*dx y0 x0+2*dx+blkW y0+blkH], ...
    'InitialCondition', '[0;0;0]');

% --- Odometry + Prior (MATLAB Function) ---
add_block('simulink/User-Defined Functions/MATLAB Function', [model '/OdometryPrior'], ...
    'Position', [x0+2*dx y0+2*dy x0+2*dx+blkW y0+2*dy+blkH]);

set_param([model '/OdometryPrior'], 'Script', sprintf([ ...
'function xprior = f(xhat_prev, u)\n' ...
'%% noisy odometry integration, dt=0.1 assumed\n' ...
'dt = 0.1;\n' ...
'sigmaV = 0.08; sigmaW = deg2rad(3);\n' ...
'v = u(1) + sigmaV*randn;\n' ...
'w = u(2) + sigmaW*randn;\n' ...
'th = xhat_prev(3);\n' ...
'x = xhat_prev(1) + v*dt*cos(th);\n' ...
'y = xhat_prev(2) + v*dt*sin(th);\n' ...
'th = wrapToPi(xhat_prev(3) + w*dt);\n' ...
'xprior = [x;y;th];\n' ...
'end\n' ...
])));

% --- Sensor (range-bearing to one landmark for simplicity) ---
add_block('simulink/User-Defined Functions/MATLAB Function', [model '/LandmarkSensor'], ...
    'Position', [x0+3*dx y0 x0+3*dx+blkW y0+blkH]);

set_param([model '/LandmarkSensor'], 'Script', sprintf([ ...
'function z = f(xtrue)\n' ...
'%% range-bearing to a single known landmark, with noise\n' ...
'lm = [6;6];\n' ...
'sigmaR = 0.12; sigmaB = deg2rad(2);\n' ...
'dx = lm(1) - xtrue(1);\n' ...
'dy = lm(2) - xtrue(2);\n' ...
'r = hypot(dx,dy);\n' ...
'b = wrapToPi(atan2(dy,dx) - xtrue(3));\n' ...
'z = [r + sigmaR*randn; wrapToPi(b + sigmaB*randn)];\n' ...
'end\n' ...
])));

% --- Estimator (one WLS/GN step) ---
add_block('simulink/User-Defined Functions/MATLAB Function', [model '/Estimator'], ...
    'Position', [x0+3*dx y0+dy x0+3*dx+blkW y0+dy+blkH]);

set_param([model '/Estimator'], 'Script', sprintf([ ...
'function xhat = f(xprior, z)\n' ...
'%% one Gauss-Newton step for pose from a single landmark range-bearing\n' ...
'lm = [6;6];\n' ...
'sigmaR = 0.12; sigmaB = deg2rad(2);\n' ...
'R = diag([sigmaR^2, sigmaB^2]);\n' ...
'\n' ...
'x = xprior; %% linearize at prior\n' ...
'dx = lm(1) - x(1);\n' ...
'dy = lm(2) - x(2);\n' ...
'q = max(dx^2 + dy^2, 1e-12);\n' ...
'rhat = sqrt(q);\n' ...
'bhat = wrapToPi(atan2(dy,dx) - x(3));\n' ...
'res = [z(1)-rhat; wrapToPi(z(2)-bhat)];\n' ...
'\n' ...
'H = [-dx/rhat, -dy/rhat, 0;\n' ...
'      dy/q,    -dx/q,  -1];\n' ...
'\n' ...
'%% simple diagonal prior weight (tunable)\n' ...
'P = diag([0.15^2, 0.15^2, deg2rad(8)^2]);\n' ...
'A = inv(P) + H''/R*H;\n' ...
'b = H''/R*res;\n' ...
'delta = A\\b;\n' ...
'xhat = x + delta;\n' ...
'xhat(3) = wrapToPi(xhat(3));\n' ...
'end\n' ...
])));

% --- Scope blocks ---
add_block('simulink/Sinks/Scope', [model '/Scope'], ...
    'Position', [x0+4*dx y0+dy x0+4*dx+blkW y0+dy+blkH]);
set_param([model '/Scope'], 'NumInputPorts', '2');

% --- Wiring ---
add_line(model, 'Goal/1', 'Controller/1');
add_line(model, 'x_hat(z^-1)/1', 'Controller/2');

add_line(model, 'Controller/1', 'Plant/2');
add_line(model, 'x_true(z^-1)/1', 'Plant/1');

add_line(model, 'Plant/1', 'x_true(z^-1)/1'); % feedback state

add_line(model, 'Controller/1', 'OdometryPrior/2');
add_line(model, 'x_hat(z^-1)/1', 'OdometryPrior/1');

add_line(model, 'x_true(z^-1)/1', 'LandmarkSensor/1');

add_line(model, 'OdometryPrior/1', 'Estimator/1');
add_line(model, 'LandmarkSensor/1', 'Estimator/2');

add_line(model, 'Estimator/1', 'x_hat(z^-1)/1'); % update estimate

add_line(model, 'x_true(z^-1)/1', 'Scope/1');
add_line(model, 'x_hat(z^-1)/1', 'Scope/2');

save_system(model);
disp('Built and saved ch1_l4_pipeline.slx');
      

Robotics libraries to know (MATLAB/Simulink): Robotics System Toolbox (transforms, localization utilities), Navigation Toolbox (maps/costmaps), and Simulink for real-time simulation and code generation.

10. Wolfram Mathematica Lab — Notebook-Style Implementation

Mathematica is useful for symbolic derivations (Jacobians, linearization checks) and quick numerical experiments. The following notebook (stored as a plain-text .nb) reproduces the WLS correction approach.

Chapter1_Lesson4.nb


(* Chapter1_Lesson4.nb
   Wolfram Mathematica notebook (plain-text representation).
   It simulates a 2D unicycle with noisy odometry and range-bearing landmark measurements,
   then performs a Gauss-Newton/WLS correction step each time.
*)

Notebook[{
  Cell["Chapter 1 — Lesson 4: Sensing–Estimation–Navigation Pipeline (Demo)", "Title"],
  Cell["This notebook is a minimal, self-contained simulation of a sensing–estimation–navigation loop.", "Text"],

  Cell[BoxData[
    ToBoxes[
      ClearAll["Global`*"];
      wrapAngle[a_] := Module[{x = Mod[a + Pi, 2 Pi]}, If[x < 0, x + 2 Pi, x] - Pi];

      unicycleStep[x_, v_, w_, dt_] := Module[{th = x[[3]]},
        {x[[1]] + v dt Cos[th], x[[2]] + v dt Sin[th], wrapAngle[x[[3]] + w dt]}
      ];

      measModel[x_, lm_] := Module[{dx = lm[[1]] - x[[1]], dy = lm[[2]] - x[[2]], r, b},
        r = Sqrt[dx^2 + dy^2];
        b = wrapAngle[ArcTan[dx, dy] - x[[3]]];
        {r, b}
      ];

      measJacobian[x_, lm_] := Module[{dx = lm[[1]] - x[[1]], dy = lm[[2]] - x[[2]], q, r},
        q = Max[dx^2 + dy^2, 10^-12];
        r = Sqrt[q];
        {
          {-dx/r, -dy/r, 0},
          {dy/q, -dx/q, -1}
        }
      ];
    ]
  ], "Input"],

  Cell["Simulation parameters", "Section"],
  Cell[BoxData[
    ToBoxes[
      SeedRandom[7];
      landmarks = { {5., 0.}, {6., 6.}, {0., 6.} };
      dt = 0.1; N = 350;

      xTrue = {0., 0., 0.};
      xHat  = {0., 0., 0.};

      P = DiagonalMatrix[{0.15^2, 0.15^2, (8 Degree)^2}];

      sigmaV = 0.08;
      sigmaW = 3 Degree;
      sigmaR = 0.12;
      sigmaB = 2 Degree;

      goal = {7., 7.};
      kHeading = 1.6;
      vMax = 0.9;

      R = DiagonalMatrix[{sigmaR^2, sigmaB^2}];
    ]
  ], "Input"],

  Cell["Iterated WLS update (prior + stacked measurements)", "Section"],
  Cell[BoxData[
    ToBoxes[
      wlsUpdateIterated[xPrior_, PPrior_, zList_, lmList_, Rmeas_, iters_: 2] :=
        Module[{m, z, Rbig, Rinv, Pinv, x, h, H, res, A, b, delta},
          m = Length[zList];
          If[m == 0, Return[{xPrior, PPrior}]];

          z = Flatten[zList];
          Rbig = KroneckerProduct[IdentityMatrix[m], Rmeas];
          Rinv = Inverse[Rbig];
          Pinv = Inverse[PPrior];

          x = xPrior;
          Do[
            h = Flatten[measModel[x, #] & /@ lmList];
            H = ArrayFlatten[Partition[measJacobian[x, lmList[[#]]], 1] & /@ Range[m]];

            res = z - h;
            (* wrap each bearing residual *)
            Do[res[[2 i]] = wrapAngle[res[[2 i]]], {i, 1, m}];

            A = Pinv + Transpose[H].Rinv.H;
            b = Transpose[H].Rinv.res + Pinv.(xPrior - x);

            delta = LinearSolve[A, b];
            x = x + delta;
            x[[3]] = wrapAngle[x[[3]]];
          , {iters}];

          (* posterior covariance at final linearization point *)
          H = ArrayFlatten[Partition[measJacobian[x, lmList[[#]]], 1] & /@ Range[m]];
          {x, Inverse[Pinv + Transpose[H].Rinv.H]}
        ];
    ]
  ], "Input"],

  Cell["Main loop", "Section"],
  Cell[BoxData[
    ToBoxes[
      histTrue = ConstantArray[0., {N, 3}];
      histHat  = ConstantArray[0., {N, 3}];

      Do[
        (* navigation uses estimate *)
        dx = goal[[1]] - xHat[[1]];
        dy = goal[[2]] - xHat[[2]];
        dist = Sqrt[dx^2 + dy^2];
        desired = ArcTan[dx, dy];
        headingErr = wrapAngle[desired - xHat[[3]]];

        vCmd = vMax Tanh[dist];
        wCmd = kHeading headingErr;

        (* true motion with mild slip *)
        slip = (0.35 Degree) RandomVariate[NormalDistribution[0, 1]];
        xTrue = unicycleStep[xTrue, vCmd, wCmd + slip/dt, dt];

        (* odometry *)
        vMeas = vCmd + sigmaV RandomVariate[NormalDistribution[0, 1]];
        wMeas = wCmd + sigmaW RandomVariate[NormalDistribution[0, 1]];

        xPrior = unicycleStep[xHat, vMeas, wMeas, dt];

        th = xHat[[3]];
        F = { {1, 0, -vMeas dt Sin[th]},
             {0, 1,  vMeas dt Cos[th]},
             {0, 0,  1} };

        Q = DiagonalMatrix[{(sigmaV dt)^2, (sigmaV dt)^2, (sigmaW dt)^2}];
        PPrior = F.P.Transpose[F] + Q;

        (* measurements: use landmarks within range *)
        maxRange = 8.;
        zList = {};
        lmList = {};
        Do[
          zTrue = measModel[xTrue, lm];
          If[zTrue[[1]] <= maxRange,
            zNoisy = {zTrue[[1]] + sigmaR RandomVariate[NormalDistribution[0, 1]],
                      wrapAngle[zTrue[[2]] + sigmaB RandomVariate[NormalDistribution[0, 1]]]};
            zList = Append[zList, zNoisy];
            lmList = Append[lmList, lm];
          ];
        , {lm, landmarks}];

        {xHat, P} = wlsUpdateIterated[xPrior, PPrior, zList, lmList, R, 2];

        histTrue[[k]] = xTrue;
        histHat[[k]] = xHat;
      , {k, 1, N}];

      ListLinePlot[{histTrue[[All, {1, 2}]], histHat[[All, {1, 2}]]},
        PlotLegends -> {"true", "estimated"},
        AspectRatio -> 1,
        GridLines -> Automatic,
        PlotRange -> All,
        AxesLabel -> {"x (m)", "y (m)"},
        PlotLabel -> "Sensing–Estimation–Navigation loop (demo)"
      ]
    ]
  ], "Input"]
}]
      

11. Problems and Solutions

Problem 1 (ML \( \Rightarrow \) WLS): Let \( \mathbf{y} = \mathbf{h}(\mathbf{x}) + \mathbf{v} \) with \( \mathbf{v} \sim \mathcal{N}(\mathbf{0},\mathbf{R}) \). Derive the maximum-likelihood estimator for \( \mathbf{x} \).

Solution: The likelihood is Gaussian with mean \( \mathbf{h}(\mathbf{x}) \). Maximizing \( p(\mathbf{y}\mid\mathbf{x}) \) is equivalent to minimizing the negative log-likelihood, which equals (up to constants):

\[ J(\mathbf{x}) = \left(\mathbf{y}-\mathbf{h}(\mathbf{x})\right)^\top \mathbf{R}^{-1} \left(\mathbf{y}-\mathbf{h}(\mathbf{x})\right). \]

Therefore \( \hat{\mathbf{x}}_{\mathrm{ML}} = \arg\min_{\mathbf{x}} J(\mathbf{x}) \).


Problem 2 (Range–bearing Jacobian): A landmark at \( \boldsymbol{\ell}=(\ell_x,\ell_y)^\top \) is observed from pose \( \mathbf{x}=(x,y,\theta)^\top \) by: \( r = \sqrt{(\ell_x-x)^2+(\ell_y-y)^2} \) and \( b = \mathrm{atan2}(\ell_y-y,\ell_x-x)-\theta \). Compute \( \mathbf{H}=\tfrac{\partial (r,b)}{\partial (x,y,\theta)} \).

Solution: Let \( d_x=\ell_x-x \), \( d_y=\ell_y-y \), \( q=d_x^2+d_y^2 \), \( r=\sqrt{q} \). Then:

\[ \mathbf{H} = \begin{bmatrix} -\tfrac{d_x}{r} & -\tfrac{d_y}{r} & 0 \\ \tfrac{d_y}{q} & -\tfrac{d_x}{q} & -1 \end{bmatrix}. \]


Problem 3 (Unobservability with range-only): Suppose measurements are range-only to any set of landmarks, i.e., \( y_i = \sqrt{(\ell_{ix}-x)^2+(\ell_{iy}-y)^2} + v_i \), with no bearing term. Prove that \( \theta \) is unobservable from these measurements.

Solution: The measurement function depends only on \( (x,y) \). Formally, for any \( \theta \) and any increment \( \Delta\theta \), we have \( \mathbf{h}(x,y,\theta) = \mathbf{h}(x,y,\theta+\Delta\theta) \). Therefore distinct states that differ only in \( \theta \) produce identical measurement predictions, so no estimator can recover \( \theta \) uniquely from range-only data. \( \square \)


Problem 4 (One-step Gauss–Newton normal equations): Consider the prior+measurement objective in Section 3 and linearize \( \mathbf{h}(\mathbf{x}) \) at \( \mathbf{x}_0 \). Derive the normal equations for the minimizing increment \( \delta \).

Solution: Using \( \mathbf{h}(\mathbf{x}_0+\delta) \approx \mathbf{h}(\mathbf{x}_0)+\mathbf{H}\delta \), the objective becomes a quadratic in \( \delta \). Setting the gradient to zero yields:

\[ \left(\mathbf{P}^{-1} + \mathbf{H}^\top\mathbf{R}^{-1}\mathbf{H}\right)\,\delta = \mathbf{H}^\top\mathbf{R}^{-1}\left(\mathbf{y}-\mathbf{h}(\mathbf{x}_0)\right) + \mathbf{P}^{-1}\left(\mathbf{x}^{-}-\mathbf{x}_0\right). \]

which matches the update used in the lab codes.


Problem 5 (Practical stability with bounded estimation error): In Section 4, assume \( \|\tilde{\mathbf{p}}_k\| \le \varepsilon \) and \( 0 \lt k\Delta t \lt 1 \). Prove that \( \|\mathbf{e}_k\| \) converges to an ultimate bound of size at most \( \varepsilon \).

Solution: The recursion is \( \mathbf{e}_{k+1} = a\mathbf{e}_k + (1-a)\tilde{\mathbf{p}}_k \) with \( a=1-k\Delta t \) and \( |a| \lt 1 \). Unrolling and bounding the geometric series gives: \( \|\mathbf{e}_k\| \le |a|^k\|\mathbf{e}_0\| + (1-a)\varepsilon\sum_{j=0}^{k-1}|a|^j \). Taking \( k\to\infty \) yields \( \limsup\|\mathbf{e}_k\| \le \varepsilon \). \( \square \)

12. Summary

We built a compact mathematical model of the sensing–estimation–navigation pipeline and showed how Gaussian measurement assumptions lead naturally to weighted least squares (with a prior term for dead-reckoning). We also proved a practical stability bound demonstrating that navigation accuracy cannot exceed estimation accuracy. The labs implemented the entire loop in Python, C++, Java, MATLAB/Simulink, and Mathematica, establishing a baseline simulation artifact that we will progressively refine in later chapters.

13. References

  1. Smith, R., Self, M., & Cheeseman, P. (1990). Estimating uncertain spatial relationships in robotics. Autonomous Robot Vehicles, 167–193.
  2. Durrant-Whyte, H.F. (1988). Uncertain geometry in robotics. IEEE Journal of Robotics and Automation, 4(1), 23–31.
  3. Leonard, J.J., & Durrant-Whyte, H.F. (1991). Mobile robot localization by tracking geometric beacons. IEEE Transactions on Robotics and Automation, 7(3), 376–382.
  4. Roumeliotis, S.I., & Bekey, G.A. (2002). Distributed multirobot localization. IEEE Transactions on Robotics and Automation, 18(5), 781–795.
  5. Kaess, M., Johannsson, H., Roberts, R., Ila, V., Leonard, J.J., & Dellaert, F. (2012). iSAM2: Incremental smoothing and mapping using the Bayes tree. International Journal of Robotics Research, 31(2), 216–235.
  6. Anderson, B.D.O., & Moore, J.B. (1979). Optimal Filtering. Prentice-Hall.