Chapter 9: Mapping Representations for Mobile Robots

Lesson 1: Occupancy Grid Mapping

This lesson introduces occupancy grids as a probabilistic representation of workspace geometry for autonomous mobile robots. We develop the binary-cell map formalism, connect mapping to Bayesian inference under modeling assumptions, and derive principled inverse-sensor updates for range sensors. We emphasize discretization, numerical stability, and the algorithmic structure required to build a map incrementally along a robot trajectory.

1. Conceptual Overview

A mobile robot operating in an unknown environment must maintain an internal model of free space and obstacles. An occupancy grid discretizes the plane into cells and assigns to each cell a probability of being occupied. The map is therefore a random field over a finite lattice.

Let the map have \( N \) cells. For each cell \( i \in \{1,\dots,N\} \), define a binary random variable \( m_i \in \{0,1\} \), where \( m_i=1 \) denotes “occupied” and \( m_i=0 \) denotes “free”. The occupancy grid stores (an estimate of) \( p(m_i=1) \) for all cells.

The key design parameters are: \( r \) (resolution, meters per cell), \( (o_x,o_y) \) (world origin of the grid), and the finite support \( [0,W)\times[0,H) \) in cell coordinates. A world point \( (x,y) \) is mapped to indices by

\[ g_x = \left\lfloor \frac{x-o_x}{r} \right\rfloor,\qquad g_y = \left\lfloor \frac{y-o_y}{r} \right\rfloor. \]

Occupancy grids are attractive because they: (i) integrate naturally with range sensors; (ii) support conservative planning via “occupied probability thresholds”; and (iii) yield an explicit representation for navigation layers (e.g., costmaps) later in the course.

flowchart TD
  S["Start"] --> P["Choose grid: resolution r, size W x H, origin (ox, oy)"]
  P --> I["Initialize prior occupancy p0 for all cells"]
  I --> T["For each time step t"]
  T --> X["Get robot pose x_t (from odometry/localization)"]
  X --> Z["Get range scan z_t (e.g., LiDAR rays)"]
  Z --> B["For each beam in the scan"]
  B --> R["Trace ray through grid cells (line traversal)"]
  R --> U["Update cells: free along ray, occupied at hit (if any)"]
  U --> B
  B --> T
  T --> O["Output occupancy grid map"]
        

2. The Mapping Posterior and Bayes Structure

Let \( x_{1:t} \) be the robot poses up to time \( t \) and \( z_{1:t} \) be the sensor measurements (e.g., range scans). The mapping problem seeks the posterior distribution \( p(m\mid x_{1:t}, z_{1:t}) \), where \( m \) denotes the entire grid map (the collection of all \( m_i \)).

By Bayes’ rule:

\[ p(m\mid x_{1:t}, z_{1:t}) = \eta\; p(z_{1:t} \mid m, x_{1:t})\; p(m), \]

where \( \eta \) is a normalizing constant. A standard modeling assumption is conditional independence of measurements over time given the map and poses:

\[ p(z_{1:t} \mid m, x_{1:t}) = \prod_{k=1}^{t} p(z_k \mid m, x_k). \]

Even with this factorization, the exact posterior over the full map is high-dimensional and intractable to update exactly. Occupancy grids therefore adopt an additional approximation: cellwise independence of the map posterior, i.e. approximate \( p(m\mid x_{1:t}, z_{1:t}) \approx \prod_{i=1}^{N} p(m_i\mid x_{1:t}, z_{1:t}) \). This approximation is not exact (sensor rays correlate cells), but it is the key to a tractable incremental update.

The core update target is each cell’s marginal posterior \( p(m_i\mid x_{1:t}, z_{1:t}) \). Under standard Bayesian filtering logic (conditioning on the newest measurement):

\[ p(m_i\mid x_{1:t}, z_{1:t}) = \eta\; p(z_t \mid m_i, x_t)\; p(m_i\mid x_{1:t-1}, z_{1:t-1}). \]

A common implementation path is to specify an inverse sensor model: \( p(m_i \mid z_t, x_t) \). Using Bayes’ rule on the likelihood term:

\[ p(z_t \mid m_i, x_t) = \frac{p(m_i \mid z_t, x_t)\, p(z_t\mid x_t)}{p(m_i)}. \]

Substituting into the filter update yields a multiplicative correction by the inverse model:

\[ p(m_i\mid x_{1:t}, z_{1:t}) = \eta\; \frac{p(m_i \mid z_t, x_t)}{p(m_i)}\; p(m_i\mid x_{1:t-1}, z_{1:t-1}). \]

This identity is the formal bridge from “sensor geometry” (inverse model) to “Bayesian update” (mapping). In practice, storing probabilities directly can be numerically fragile when probabilities approach 0 or 1. A numerically stable alternative is to work with odds and log-odds; the full log-odds development and efficient incremental formulas are the focus of Lesson 2, but we provide the essential algebra here to motivate the implementations.

3. Odds Form and a Short Proof Sketch

Define the odds of occupancy for cell \( i \) at time \( t \): \( O_t(i) = \dfrac{p(m_i=1 \mid x_{1:t}, z_{1:t})}{p(m_i=0 \mid x_{1:t}, z_{1:t})} \). Also define the prior odds \( O_0(i) = \dfrac{p(m_i=1)}{p(m_i=0)} \) and the inverse-model odds \( O_{inv}(i) = \dfrac{p(m_i=1 \mid z_t, x_t)}{p(m_i=0 \mid z_t, x_t)} \).

Proposition (odds update). Under the per-cell update equation in Section 2, the posterior odds satisfy:

\[ O_t(i) = O_{t-1}(i)\, \frac{O_{inv}(i)}{O_0(i)}. \]

Proof sketch. Start from \( p(m_i\mid x_{1:t}, z_{1:t}) \propto \frac{p(m_i\mid z_t, x_t)}{p(m_i)}\, p(m_i\mid x_{1:t-1}, z_{1:t-1}) \). Write the same relation for \( m_i=1 \) and \( m_i=0 \), take their ratio, and note that the normalizer \( \eta \) and the factor \( p(z_t\mid x_t) \) cancel. The remaining ratio is exactly the odds update above.

Taking logs converts the multiplicative update into an additive update:

\[ \ell_t(i) = \log O_t(i) = \ell_{t-1}(i) + \log O_{inv}(i) - \log O_0(i). \]

where \( \ell_t(i) \) is the log-odds. This is the computational form used by most occupancy grid mappers (including the provided code), because it is stable and efficient.

4. Inverse Sensor Model for a Planar Range Sensor

Consider a planar range sensor producing beams indexed by \( k \). Each beam returns a range \( z_t^{(k)} \in [0, z_{max}] \) at bearing \( \phi^{(k)} \) relative to the robot heading. Let the robot pose be \( x_t = (x_t, y_t, \theta_t) \). The beam direction in the world is \( \alpha^{(k)} = \theta_t + \phi^{(k)} \).

A simple inverse model partitions cells near the ray into three classes: (i) cells before the hit are likely free; (ii) cells at the hit are likely occupied (if a hit occurs before max range); (iii) other cells are left at the prior. Let \( d_i \) denote the distance from the sensor origin to the center of cell \( i \) projected along the ray direction (for cells on the ray). Choose a “thickness” parameter \( \alpha > 0 \) (often on the order of one cell). Then a canonical piecewise inverse model is:

\[ p(m_i=1 \mid z_t^{(k)}, x_t) = \begin{cases} p_{free} & \text{if } d_i < z_t^{(k)} - \alpha/2 \\ p_{occ} & \text{if } \bigl|d_i - z_t^{(k)}\bigr| \le \alpha/2 \;\text{ and }\; z_t^{(k)} < z_{max} \\ p_0 & \text{otherwise} \end{cases} \]

with design choices \( p_{occ} > 0.5 \) and \( p_{free} < 0.5 \). The “otherwise” case covers cells not intersected by the ray, cells beyond the observed range, and max-range returns (no obstacle hit). In a discrete grid, the set of intersected cells is computed by a line traversal algorithm (e.g., Bresenham).

In log-odds form with prior \( p_0 \), the per-cell update implied by this inverse model is: if a cell is labeled “free”, add \( \operatorname{logit}(p_{free}) - \operatorname{logit}(p_0) \); if “occupied”, add \( \operatorname{logit}(p_{occ}) - \operatorname{logit}(p_0) \); otherwise add 0. This is exactly what the implementations do.

flowchart TD
  A["Inputs: pose (x,y,theta), beam angle phi, range z, max range zMax"] --> B["Compute world beam angle a = theta + phi"]
  B --> C["Compute endpoint (xe, ye) = (x + z*cos(a), y + z*sin(a))"]
  C --> D["Traverse grid cells along segment start->end"]
  D --> E["If z below zMax: \nmark last cell as occupied"]
  D --> F["Mark all preceding cells as free"]
  E --> G["Clamp logOdds to [Lmin, Lmax]"]
  F --> G
  G --> H["Continue with next beam"]
        

5. Practical Engineering Notes

Resolution vs computational load. If the mapped rectangle has physical size \( L_x \times L_y \), then the grid has \( W = \lfloor L_x/r \rfloor \) columns and \( H = \lfloor L_y/r \rfloor \) rows, so the cell count is \( N = WH \). Memory and update time scale roughly linearly with \( N \) for storage, and with \( t \times K \times \bar{L} \) for updates, where \( t \) is time steps, \( K \) beams per scan, and \( \bar{L} \) average number of traversed cells per beam.

Clamping / saturation. In log-odds form, repeated evidence can drive \( \ell_t(i) \) to large magnitude. Practical mappers clamp log-odds: \( \ell_t(i) ← \min(\ell_{max}, \max(\ell_{min}, \ell_t(i))) \). This prevents numerical overflow and avoids overconfidence under model mismatch.

Static-world assumption. Classic occupancy grids assume the map is static while the robot moves. In real deployments, people and moving obstacles violate this assumption and create “ghost” artifacts. Later chapters address navigation stacks that mitigate dynamics via temporal filtering and layered costmaps.

6. Implementations

The following implementations all follow the same core structure: (i) store an occupancy grid as log-odds; (ii) simulate range rays in a small synthetic environment; (iii) update free/occupied cells along each ray; and (iv) visualize or export the resulting occupancy probability map.

6.1 Python

Chapter9_Lesson1.py


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Chapter9_Lesson1.py
#
# Occupancy Grid Mapping (2D) — reference implementation for a university-level AMR course.
# This script simulates a simple environment, a robot trajectory, synthetic 2D LiDAR scans,
# and builds an occupancy grid map using log-odds updates.

from __future__ import annotations
import math
from dataclasses import dataclass
from typing import List, Tuple

import numpy as np
import matplotlib.pyplot as plt


def logit(p: float) -> float:
    p = float(np.clip(p, 1e-9, 1 - 1e-9))
    return math.log(p / (1.0 - p))


def sigmoid(x: float) -> float:
    # numerically stable sigmoid for scalar
    if x >= 0:
        z = math.exp(-x)
        return 1.0 / (1.0 + z)
    z = math.exp(x)
    return z / (1.0 + z)


@dataclass
class GridSpec:
    width: int           # number of columns
    height: int          # number of rows
    resolution: float    # meters per cell
    origin_x: float      # world x of grid cell (0,0) lower-left
    origin_y: float      # world y of grid cell (0,0) lower-left


class OccupancyGrid:
    """
    Stores log-odds for each cell.
    Probability can be obtained via p = sigmoid(l).
    """

    def __init__(
        self,
        spec: GridSpec,
        p0: float = 0.5,
        p_occ: float = 0.7,
        p_free: float = 0.3,
        l_min: float = -10.0,
        l_max: float = 10.0,
    ):
        self.spec = spec
        self.l0 = logit(p0)
        self.l_occ = logit(p_occ)
        self.l_free = logit(p_free)
        self.l_min = l_min
        self.l_max = l_max
        self.log_odds = np.full((spec.height, spec.width), self.l0, dtype=np.float64)

    def world_to_grid(self, x: float, y: float) -> Tuple[int, int]:
        gx = int(math.floor((x - self.spec.origin_x) / self.spec.resolution))
        gy = int(math.floor((y - self.spec.origin_y) / self.spec.resolution))
        return gx, gy

    def grid_to_world_center(self, gx: int, gy: int) -> Tuple[float, float]:
        x = self.spec.origin_x + (gx + 0.5) * self.spec.resolution
        y = self.spec.origin_y + (gy + 0.5) * self.spec.resolution
        return x, y

    def in_bounds(self, gx: int, gy: int) -> bool:
        return (0 <= gx < self.spec.width) and (0 <= gy < self.spec.height)

    def clamp(self):
        np.clip(self.log_odds, self.l_min, self.l_max, out=self.log_odds)

    def update_cell_logodds(self, gx: int, gy: int, l_update: float):
        if not self.in_bounds(gx, gy):
            return
        self.log_odds[gy, gx] += (l_update - self.l0)  # standard log-odds increment
        # (equivalent to l_t = l_{t-1} + l_inv - l0)
        # clamp per-update for safety
        if self.log_odds[gy, gx] > self.l_max:
            self.log_odds[gy, gx] = self.l_max
        elif self.log_odds[gy, gx] < self.l_min:
            self.log_odds[gy, gx] = self.l_min

    def probability_grid(self) -> np.ndarray:
        # sigmoid applied elementwise
        return 1.0 / (1.0 + np.exp(-self.log_odds))


def bresenham(x0: int, y0: int, x1: int, y1: int) -> List[Tuple[int, int]]:
    """
    Bresenham line traversal returning grid indices from (x0,y0) to (x1,y1), inclusive.
    """
    points: List[Tuple[int, int]] = []
    dx = abs(x1 - x0)
    dy = abs(y1 - y0)
    sx = 1 if x0 < x1 else -1
    sy = 1 if y0 < y1 else -1
    err = dx - dy
    x, y = x0, y0
    while True:
        points.append((x, y))
        if x == x1 and y == y1:
            break
        e2 = 2 * err
        if e2 > -dy:
            err -= dy
            x += sx
        if e2 < dx:
            err += dx
            y += sy
    return points


# --- Simple environment and ray casting for synthetic LiDAR --- #

@dataclass
class Segment:
    x0: float
    y0: float
    x1: float
    y1: float


def ray_segment_intersection(
    rx: float, ry: float, rdx: float, rdy: float,
    s: Segment
) -> float | None:
    """
    Ray (rx,ry) + t*(rdx,rdy), t>=0 with segment s.
    Returns smallest positive t where intersection occurs, else None.
    """
    x1, y1, x2, y2 = s.x0, s.y0, s.x1, s.y1
    # Solve intersection between parametric ray and segment
    # Using 2D cross product approach:
    # ray: p + t r, segment: q + u s
    # t = (q - p) x s / (r x s), u = (q - p) x r / (r x s)
    rxs = rdx * (y2 - y1) - rdy * (x2 - x1)
    if abs(rxs) < 1e-12:
        return None  # parallel
    qpx = x1 - rx
    qpy = y1 - ry
    t = (qpx * (y2 - y1) - qpy * (x2 - x1)) / rxs
    u = (qpx * rdy - qpy * rdx) / rxs
    if t >= 0.0 and 0.0 <= u <= 1.0:
        return t
    return None


def simulate_lidar_scan(
    pose: Tuple[float, float, float],
    segments: List[Segment],
    angles: np.ndarray,
    z_max: float,
) -> np.ndarray:
    """
    Returns ranges for each beam angle relative to robot heading.
    """
    x, y, th = pose
    ranges = np.full_like(angles, z_max, dtype=np.float64)
    for i, a in enumerate(angles):
        wa = th + a
        rdx, rdy = math.cos(wa), math.sin(wa)
        t_min = None
        for seg in segments:
            t = ray_segment_intersection(x, y, rdx, rdy, seg)
            if t is not None and t <= z_max:
                if (t_min is None) or (t < t_min):
                    t_min = t
        if t_min is not None:
            ranges[i] = t_min
    return ranges


def build_box_world(xmin: float, ymin: float, xmax: float, ymax: float) -> List[Segment]:
    """
    Axis-aligned rectangular boundary as four segments.
    """
    return [
        Segment(xmin, ymin, xmax, ymin),
        Segment(xmax, ymin, xmax, ymax),
        Segment(xmax, ymax, xmin, ymax),
        Segment(xmin, ymax, xmin, ymin),
    ]


def add_vertical_wall(x: float, y0: float, y1: float) -> Segment:
    return Segment(x, y0, x, y1)


def add_horizontal_wall(y: float, x0: float, x1: float) -> Segment:
    return Segment(x0, y, x1, y)


def update_grid_from_scan(
    grid: OccupancyGrid,
    pose: Tuple[float, float, float],
    angles: np.ndarray,
    ranges: np.ndarray,
    z_max: float,
):
    """
    Inverse sensor model:
    - cells along the ray up to (hit cell exclusive) are updated as FREE
    - the end cell is updated as OCCUPIED if range < z_max (hit)
    """
    x, y, th = pose
    gx0, gy0 = grid.world_to_grid(x, y)

    for a, r in zip(angles, ranges):
        wa = th + float(a)
        ex = x + float(r) * math.cos(wa)
        ey = y + float(r) * math.sin(wa)

        gx1, gy1 = grid.world_to_grid(ex, ey)

        # traverse cells from robot to endpoint
        cells = bresenham(gx0, gy0, gx1, gy1)
        if len(cells) == 0:
            continue

        hit = (float(r) < float(z_max) - 1e-12)

        # free cells: all but last (or all if no hit, conservative: treat all traversed as free)
        free_cells = cells[:-1] if hit else cells
        for (gx, gy) in free_cells:
            grid.update_cell_logodds(gx, gy, grid.l_free)

        # occupied: last cell only if hit
        if hit:
            lx, ly = cells[-1]
            grid.update_cell_logodds(lx, ly, grid.l_occ)


def main():
    # --- Map and sensor settings ---
    spec = GridSpec(
        width=220,
        height=180,
        resolution=0.1,
        origin_x=-11.0,
        origin_y=-9.0
    )
    grid = OccupancyGrid(spec, p0=0.5, p_occ=0.75, p_free=0.30, l_min=-8.0, l_max=8.0)

    # LiDAR: 180 deg scan, 181 beams
    fov = math.radians(180.0)
    n_beams = 181
    angles = np.linspace(-fov / 2.0, fov / 2.0, n_beams)
    z_max = 10.0

    # --- Synthetic environment (walls) ---
    segments: List[Segment] = []
    segments += build_box_world(-10.0, -8.0, 10.0, 8.0)   # boundary
    segments.append(add_vertical_wall(0.0, -6.0, 6.0))    # internal wall
    segments.append(add_horizontal_wall(2.0, -8.0, -2.0)) # short wall

    # --- Robot trajectory (simple loop) ---
    poses: List[Tuple[float, float, float]] = []
    for t in range(120):
        # move on an ellipse, slowly rotating heading
        ang = 2.0 * math.pi * (t / 120.0)
        x = -6.0 + 6.0 * math.cos(ang)
        y =  0.0 + 4.0 * math.sin(ang)
        th = ang + math.pi / 2.0
        poses.append((x, y, th))

    # --- Mapping loop ---
    for pose in poses:
        ranges = simulate_lidar_scan(pose, segments, angles, z_max)
        # optional: add small noise
        ranges = np.clip(ranges + np.random.normal(0.0, 0.02, size=ranges.shape), 0.0, z_max)
        update_grid_from_scan(grid, pose, angles, ranges, z_max)

    # --- Visualization ---
    p = grid.probability_grid()
    plt.figure(figsize=(9, 6))
    plt.imshow(
        p,
        origin="lower",
        extent=[
            spec.origin_x,
            spec.origin_x + spec.width * spec.resolution,
            spec.origin_y,
            spec.origin_y + spec.height * spec.resolution,
        ],
        vmin=0.0, vmax=1.0
    )
    plt.colorbar(label="P(occupied)")
    plt.title("Occupancy Grid Map (probability)")
    plt.xlabel("x [m]")
    plt.ylabel("y [m]")
    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    main()
      

6.2 C++

Chapter9_Lesson1.cpp


// Chapter9_Lesson1.cpp
//
// Occupancy Grid Mapping (2D) — C++ reference implementation.
// Simulates a simple environment, generates synthetic 2D LiDAR scans,
// and builds an occupancy grid map using log-odds updates.
//
// Build (example):
//   g++ -O2 -std=c++17 Chapter9_Lesson1.cpp -o Chapter9_Lesson1
// Run:
//   ./Chapter9_Lesson1

#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <random>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

struct GridSpec {
  int width;
  int height;
  double resolution;
  double origin_x;
  double origin_y;
};

static inline double clamp(double x, double lo, double hi) {
  if (x < lo) return lo;
  if (x > hi) return hi;
  return x;
}

static inline double logit(double p) {
  p = clamp(p, 1e-9, 1.0 - 1e-9);
  return std::log(p / (1.0 - p));
}

static inline double sigmoid(double x) {
  // stable sigmoid
  if (x >= 0.0) {
    double z = std::exp(-x);
    return 1.0 / (1.0 + z);
  }
  double z = std::exp(x);
  return z / (1.0 + z);
}

struct OccupancyGrid {
  GridSpec spec;
  double l0;
  double l_occ;
  double l_free;
  double l_min;
  double l_max;
  std::vector<double> log_odds; // row-major [gy*W + gx]

  OccupancyGrid(const GridSpec& s,
                double p0 = 0.5,
                double p_occ = 0.75,
                double p_free = 0.30,
                double lmin = -8.0,
                double lmax = 8.0)
      : spec(s),
        l0(logit(p0)),
        l_occ(logit(p_occ)),
        l_free(logit(p_free)),
        l_min(lmin),
        l_max(lmax),
        log_odds(static_cast<size_t>(s.width * s.height), l0) {}

  inline bool in_bounds(int gx, int gy) const {
    return (0 <= gx && gx < spec.width && 0 <= gy && gy < spec.height);
  }

  inline std::pair<int,int> world_to_grid(double x, double y) const {
    int gx = static_cast<int>(std::floor((x - spec.origin_x) / spec.resolution));
    int gy = static_cast<int>(std::floor((y - spec.origin_y) / spec.resolution));
    return {gx, gy};
  }

  inline void update_cell(int gx, int gy, double l_update) {
    if (!in_bounds(gx, gy)) return;
    size_t idx = static_cast<size_t>(gy * spec.width + gx);
    log_odds[idx] += (l_update - l0);
    if (log_odds[idx] > l_max) log_odds[idx] = l_max;
    if (log_odds[idx] < l_min) log_odds[idx] = l_min;
  }

  inline double prob(int gx, int gy) const {
    if (!in_bounds(gx, gy)) return 0.5;
    size_t idx = static_cast<size_t>(gy * spec.width + gx);
    return sigmoid(log_odds[idx]);
  }
};

// Bresenham line traversal (inclusive endpoints)
static std::vector<std::pair<int,int>> bresenham(int x0, int y0, int x1, int y1) {
  std::vector<std::pair<int,int>> pts;
  int dx = std::abs(x1 - x0);
  int dy = std::abs(y1 - y0);
  int sx = (x0 < x1) ? 1 : -1;
  int sy = (y0 < y1) ? 1 : -1;
  int err = dx - dy;
  int x = x0, y = y0;
  for (;;) {
    pts.push_back({x, y});
    if (x == x1 && y == y1) break;
    int e2 = 2 * err;
    if (e2 > -dy) { err -= dy; x += sx; }
    if (e2 <  dx) { err += dx; y += sy; }
  }
  return pts;
}

struct Segment {
  double x0, y0, x1, y1;
};

// Ray-segment intersection: ray p + t*r, t>=0. Return smallest t if intersects segment.
static double ray_segment_intersection(double rx, double ry, double rdx, double rdy,
                                      const Segment& s, bool& hit) {
  // Cross product approach
  double x1 = s.x0, y1 = s.y0, x2 = s.x1, y2 = s.y1;
  double sx = x2 - x1;
  double sy = y2 - y1;
  double rxs = rdx * sy - rdy * sx;
  if (std::abs(rxs) < 1e-12) { hit = false; return 0.0; } // parallel
  double qpx = x1 - rx;
  double qpy = y1 - ry;
  double t = (qpx * sy - qpy * sx) / rxs;
  double u = (qpx * rdy - qpy * rdx) / rxs;
  if (t >= 0.0 && u >= 0.0 && u <= 1.0) { hit = true; return t; }
  hit = false; return 0.0;
}

static std::vector<Segment> build_box_world(double xmin, double ymin, double xmax, double ymax) {
  return {
    {xmin, ymin, xmax, ymin},
    {xmax, ymin, xmax, ymax},
    {xmax, ymax, xmin, ymax},
    {xmin, ymax, xmin, ymin}
  };
}

static Segment vertical_wall(double x, double y0, double y1) {
  return {x, y0, x, y1};
}

static Segment horizontal_wall(double y, double x0, double x1) {
  return {x0, y, x1, y};
}

static std::vector<double> simulate_lidar_scan(const std::tuple<double,double,double>& pose,
                                        const std::vector<Segment>& segs,
                                        const std::vector<double>& angles,
                                        double z_max) {
  double x = std::get<0>(pose);
  double y = std::get<1>(pose);
  double th = std::get<2>(pose);

  std::vector<double> ranges(angles.size(), z_max);
  for (size_t i = 0; i < angles.size(); ++i) {
    double wa = th + angles[i];
    double rdx = std::cos(wa);
    double rdy = std::sin(wa);

    double tmin = std::numeric_limits<double>::infinity();
    bool any = false;
    for (const auto& s : segs) {
      bool hit = false;
      double t = ray_segment_intersection(x, y, rdx, rdy, s, hit);
      if (hit && t <= z_max && t < tmin) {
        tmin = t;
        any = true;
      }
    }
    if (any) ranges[i] = tmin;
  }
  return ranges;
}

static void update_from_scan(OccupancyGrid& grid,
                             const std::tuple<double,double,double>& pose,
                             const std::vector<double>& angles,
                             const std::vector<double>& ranges,
                             double z_max) {
  double x = std::get<0>(pose);
  double y = std::get<1>(pose);
  double th = std::get<2>(pose);

  auto [gx0, gy0] = grid.world_to_grid(x, y);

  for (size_t i = 0; i < angles.size(); ++i) {
    double a = angles[i];
    double r = ranges[i];
    double wa = th + a;
    double ex = x + r * std::cos(wa);
    double ey = y + r * std::sin(wa);

    auto [gx1, gy1] = grid.world_to_grid(ex, ey);
    auto cells = bresenham(gx0, gy0, gx1, gy1);
    if (cells.empty()) continue;

    bool hit = (r < (z_max - 1e-12));
    // Free cells: all but last if hit; otherwise all
    size_t free_count = hit ? (cells.size() - 1) : cells.size();
    for (size_t k = 0; k < free_count; ++k) {
      grid.update_cell(cells[k].first, cells[k].second, grid.l_free);
    }
    // Occupied: last if hit
    if (hit) {
      auto [lx, ly] = cells.back();
      grid.update_cell(lx, ly, grid.l_occ);
    }
  }
}

int main() {
  GridSpec spec{220, 180, 0.1, -11.0, -9.0};
  OccupancyGrid grid(spec, 0.5, 0.75, 0.30, -8.0, 8.0);

  // LiDAR angles: 180 deg, 181 beams
  const double fov = M_PI; // 180 deg
  const int n_beams = 181;
  std::vector<double> angles;
  angles.reserve(static_cast<size_t>(n_beams));
  for (int i = 0; i < n_beams; ++i) {
    double a = -fov/2.0 + (fov * i) / (n_beams - 1);
    angles.push_back(a);
  }
  const double z_max = 10.0;

  // Environment
  std::vector<Segment> segs = build_box_world(-10.0, -8.0, 10.0, 8.0);
  segs.push_back(vertical_wall(0.0, -6.0, 6.0));
  segs.push_back(horizontal_wall(2.0, -8.0, -2.0));

  // Trajectory
  std::vector<std::tuple<double,double,double>> poses;
  const int T = 120;
  poses.reserve(static_cast<size_t>(T));
  for (int t = 0; t < T; ++t) {
    double ang = 2.0 * M_PI * (static_cast<double>(t) / T);
    double x = -6.0 + 6.0 * std::cos(ang);
    double y =  0.0 + 4.0 * std::sin(ang);
    double th = ang + M_PI/2.0;
    poses.emplace_back(x, y, th);
  }

  // Noise
  std::mt19937_64 rng(0);
  std::normal_distribution<double> noise(0.0, 0.02);

  // Mapping loop
  for (const auto& pose : poses) {
    auto ranges = simulate_lidar_scan(pose, segs, angles, z_max);
    for (auto& r : ranges) {
      r = clamp(r + noise(rng), 0.0, z_max);
    }
    update_from_scan(grid, pose, angles, ranges, z_max);
  }

  // Minimal output: write occupancy probabilities as ASCII PGM-like grayscale (0..255)
  // This keeps the example dependency-free.
  // Occupied high probability -> darker pixel (closer to 0).
  std::cout << "P2\n" << spec.width << " " << spec.height << "\n255\n";
  for (int gy = spec.height - 1; gy >= 0; --gy) { // print top-to-bottom
    for (int gx = 0; gx < spec.width; ++gx) {
      double p = grid.prob(gx, gy);
      int pix = static_cast<int>(std::round((1.0 - p) * 255.0));
      pix = static_cast<int>(clamp(pix, 0, 255));
      std::cout << pix << " ";
    }
    std::cout << "\n";
  }

  std::cerr << "Wrote ASCII PGM to stdout. Save as map.pgm and view with an image viewer.\n";
  std::cerr << "Example: ./Chapter9_Lesson1 > map.pgm\n";
  return 0;
}
      

6.3 Java

Chapter9_Lesson1.java


// Chapter9_Lesson1.java
//
// Occupancy Grid Mapping (2D) — Java reference implementation.
// Simulates a simple environment, generates synthetic 2D LiDAR scans,
// and builds an occupancy grid map using log-odds updates.
// Output: ASCII PGM written to stdout (portable, dependency-free).
//
// Compile:
//   javac Chapter9_Lesson1.java
// Run:
//   java Chapter9_Lesson1 > map.pgm

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;

public class Chapter9_Lesson1 {

    static class GridSpec {
        int width, height;
        double resolution;
        double originX, originY;

        GridSpec(int w, int h, double res, double ox, double oy) {
            width = w; height = h; resolution = res; originX = ox; originY = oy;
        }
    }

    static double clamp(double x, double lo, double hi) {
        return Math.max(lo, Math.min(hi, x));
    }

    static double logit(double p) {
        p = clamp(p, 1e-9, 1.0 - 1e-9);
        return Math.log(p / (1.0 - p));
    }

    static double sigmoid(double x) {
        if (x >= 0.0) {
            double z = Math.exp(-x);
            return 1.0 / (1.0 + z);
        } else {
            double z = Math.exp(x);
            return z / (1.0 + z);
        }
    }

    static class OccupancyGrid {
        GridSpec spec;
        double l0, lOcc, lFree, lMin, lMax;
        double[] logOdds; // row-major

        OccupancyGrid(GridSpec spec, double p0, double pOcc, double pFree, double lMin, double lMax) {
            this.spec = spec;
            this.l0 = logit(p0);
            this.lOcc = logit(pOcc);
            this.lFree = logit(pFree);
            this.lMin = lMin;
            this.lMax = lMax;
            this.logOdds = new double[spec.width * spec.height];
            for (int i = 0; i < logOdds.length; i++) logOdds[i] = l0;
        }

        boolean inBounds(int gx, int gy) {
            return (0 <= gx && gx < spec.width && 0 <= gy && gy < spec.height);
        }

        int[] worldToGrid(double x, double y) {
            int gx = (int)Math.floor((x - spec.originX) / spec.resolution);
            int gy = (int)Math.floor((y - spec.originY) / spec.resolution);
            return new int[]{gx, gy};
        }

        void updateCell(int gx, int gy, double lUpdate) {
            if (!inBounds(gx, gy)) return;
            int idx = gy * spec.width + gx;
            logOdds[idx] += (lUpdate - l0);
            if (logOdds[idx] > lMax) logOdds[idx] = lMax;
            if (logOdds[idx] < lMin) logOdds[idx] = lMin;
        }

        double prob(int gx, int gy) {
            if (!inBounds(gx, gy)) return 0.5;
            int idx = gy * spec.width + gx;
            return sigmoid(logOdds[idx]);
        }
    }

    static class Segment {
        double x0, y0, x1, y1;
        Segment(double x0, double y0, double x1, double y1) {
            this.x0=x0; this.y0=y0; this.x1=x1; this.y1=y1;
        }
    }

    // Ray/segment intersection. Returns t (distance along ray) if intersects, otherwise NaN.
    static double raySegmentIntersection(double rx, double ry, double rdx, double rdy, Segment s) {
        double sx = s.x1 - s.x0;
        double sy = s.y1 - s.y0;
        double rxs = rdx * sy - rdy * sx;
        if (Math.abs(rxs) < 1e-12) return Double.NaN; // parallel
        double qpx = s.x0 - rx;
        double qpy = s.y0 - ry;
        double t = (qpx * sy - qpy * sx) / rxs;
        double u = (qpx * rdy - qpy * rdx) / rxs;
        if (t >= 0.0 && u >= 0.0 && u <= 1.0) return t;
        return Double.NaN;
    }

    static List<Segment> buildBoxWorld(double xmin, double ymin, double xmax, double ymax) {
        List<Segment> segs = new ArrayList<>();
        segs.add(new Segment(xmin, ymin, xmax, ymin));
        segs.add(new Segment(xmax, ymin, xmax, ymax));
        segs.add(new Segment(xmax, ymax, xmin, ymax));
        segs.add(new Segment(xmin, ymax, xmin, ymin));
        return segs;
    }

    static Segment verticalWall(double x, double y0, double y1) { return new Segment(x, y0, x, y1); }
    static Segment horizontalWall(double y, double x0, double x1) { return new Segment(x0, y, x1, y); }

    static double[] simulateLidarScan(double x, double y, double th, List<Segment> segs, double[] angles, double zMax) {
        double[] ranges = new double[angles.length];
        for (int i = 0; i < ranges.length; i++) ranges[i] = zMax;

        for (int i = 0; i < angles.length; i++) {
            double wa = th + angles[i];
            double rdx = Math.cos(wa);
            double rdy = Math.sin(wa);

            double tmin = Double.POSITIVE_INFINITY;
            boolean any = false;
            for (Segment s : segs) {
                double t = raySegmentIntersection(x, y, rdx, rdy, s);
                if (!Double.isNaN(t) && t <= zMax && t < tmin) {
                    tmin = t;
                    any = true;
                }
            }
            if (any) ranges[i] = tmin;
        }
        return ranges;
    }

    // Bresenham inclusive
    static List<int[]> bresenham(int x0, int y0, int x1, int y1) {
        List<int[]> pts = new ArrayList<>();
        int dx = Math.abs(x1 - x0);
        int dy = Math.abs(y1 - y0);
        int sx = (x0 < x1) ? 1 : -1;
        int sy = (y0 < y1) ? 1 : -1;
        int err = dx - dy;
        int x = x0, y = y0;
        while (true) {
            pts.add(new int[]{x, y});
            if (x == x1 && y == y1) break;
            int e2 = 2 * err;
            if (e2 > -dy) { err -= dy; x += sx; }
            if (e2 <  dx) { err += dx; y += sy; }
        }
        return pts;
    }

    static void updateFromScan(OccupancyGrid grid, double x, double y, double th,
                               double[] angles, double[] ranges, double zMax) {
        int[] g0 = grid.worldToGrid(x, y);
        int gx0 = g0[0], gy0 = g0[1];

        for (int i = 0; i < angles.length; i++) {
            double a = angles[i];
            double r = ranges[i];
            double wa = th + a;
            double ex = x + r * Math.cos(wa);
            double ey = y + r * Math.sin(wa);

            int[] g1 = grid.worldToGrid(ex, ey);
            int gx1 = g1[0], gy1 = g1[1];

            List<int[]> cells = bresenham(gx0, gy0, gx1, gy1);
            if (cells.isEmpty()) continue;

            boolean hit = (r < (zMax - 1e-12));
            int freeCount = hit ? (cells.size() - 1) : cells.size();

            for (int k = 0; k < freeCount; k++) {
                int[] c = cells.get(k);
                grid.updateCell(c[0], c[1], grid.lFree);
            }
            if (hit) {
                int[] last = cells.get(cells.size() - 1);
                grid.updateCell(last[0], last[1], grid.lOcc);
            }
        }
    }

    public static void main(String[] args) {
        Locale.setDefault(Locale.US);

        GridSpec spec = new GridSpec(220, 180, 0.1, -11.0, -9.0);
        OccupancyGrid grid = new OccupancyGrid(spec, 0.5, 0.75, 0.30, -8.0, 8.0);

        // LiDAR angles: 180 deg, 181 beams
        int nBeams = 181;
        double fov = Math.PI;
        double[] angles = new double[nBeams];
        for (int i = 0; i < nBeams; i++) {
            angles[i] = -fov/2.0 + (fov * i) / (nBeams - 1);
        }
        double zMax = 10.0;

        // Environment
        List<Segment> segs = buildBoxWorld(-10.0, -8.0, 10.0, 8.0);
        segs.add(verticalWall(0.0, -6.0, 6.0));
        segs.add(horizontalWall(2.0, -8.0, -2.0));

        // Trajectory and noise
        Random rng = new Random(0);
        int T = 120;
        double noiseStd = 0.02;

        for (int t = 0; t < T; t++) {
            double ang = 2.0 * Math.PI * ((double)t / (double)T);
            double x = -6.0 + 6.0 * Math.cos(ang);
            double y =  0.0 + 4.0 * Math.sin(ang);
            double th = ang + Math.PI/2.0;

            double[] ranges = simulateLidarScan(x, y, th, segs, angles, zMax);
            for (int i = 0; i < ranges.length; i++) {
                // gaussian noise (Box-Muller)
                double u1 = Math.max(1e-12, rng.nextDouble());
                double u2 = rng.nextDouble();
                double z = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
                ranges[i] = clamp(ranges[i] + noiseStd * z, 0.0, zMax);
            }
            updateFromScan(grid, x, y, th, angles, ranges, zMax);
        }

        // Output ASCII PGM to stdout (P2)
        System.out.println("P2");
        System.out.println(spec.width + " " + spec.height);
        System.out.println("255");
        for (int gy = spec.height - 1; gy >= 0; gy--) { // top to bottom
            StringBuilder sb = new StringBuilder();
            for (int gx = 0; gx < spec.width; gx++) {
                double p = grid.prob(gx, gy);
                int pix = (int)Math.round((1.0 - p) * 255.0);
                pix = (int)clamp(pix, 0, 255);
                sb.append(pix).append(' ');
            }
            System.out.println(sb.toString());
        }

        System.err.println("Wrote ASCII PGM to stdout. Save as map.pgm and view with an image viewer.");
        System.err.println("Example: java Chapter9_Lesson1 > map.pgm");
    }
}
      

6.4 MATLAB / Simulink

Chapter9_Lesson1.m


% Chapter9_Lesson1.m
%
% Occupancy Grid Mapping (2D) — MATLAB reference implementation.
% Simulates a simple environment, generates synthetic 2D LiDAR scans,
% and builds an occupancy grid map using log-odds updates.
%
% Run:
%   Chapter9_Lesson1

function Chapter9_Lesson1()

  % --- Map / sensor settings ---
  spec.width = 220;
  spec.height = 180;
  spec.res = 0.1;
  spec.ox = -11.0;
  spec.oy = -9.0;

  p0 = 0.5;
  pOcc = 0.75;
  pFree = 0.30;
  l0 = logit(p0);
  lOcc = logit(pOcc);
  lFree = logit(pFree);
  lMin = -8.0;
  lMax = 8.0;

  logOdds = l0 * ones(spec.height, spec.width);

  % LiDAR: 180 deg, 181 beams
  fov = pi;
  nBeams = 181;
  angles = linspace(-fov/2, fov/2, nBeams);
  zMax = 10.0;

  % --- Synthetic environment: boundary + internal walls (segments) ---
  segs = {};
  segs = [segs; buildBoxWorld(-10.0, -8.0, 10.0, 8.0)];
  segs{end+1} = struct('x0',0.0,'y0',-6.0,'x1',0.0,'y1',6.0);     % vertical wall
  segs{end+1} = struct('x0',-8.0,'y0',2.0,'x1',-2.0,'y1',2.0);    % horizontal wall

  % --- Trajectory ---
  T = 120;
  poses = zeros(T,3);
  for t = 1:T
    ang = 2*pi*(t-1)/T;
    poses(t,1) = -6.0 + 6.0*cos(ang);
    poses(t,2) =  0.0 + 4.0*sin(ang);
    poses(t,3) = ang + pi/2;
  end

  rng(0);

  % --- Mapping loop ---
  for t = 1:T
    x = poses(t,1); y = poses(t,2); th = poses(t,3);
    ranges = simulateLidarScan([x y th], segs, angles, zMax);
    ranges = min(max(ranges + 0.02*randn(size(ranges)), 0.0), zMax);
    logOdds = updateFromScan(logOdds, spec, l0, lOcc, lFree, lMin, lMax, [x y th], angles, ranges, zMax);
  end

  % --- Visualization ---
  p = 1.0 ./ (1.0 + exp(-logOdds));
  figure; imagesc( ...
    [spec.ox, spec.ox + spec.width*spec.res], ...
    [spec.oy, spec.oy + spec.height*spec.res], ...
    p ...
  );
  set(gca,'YDir','normal');
  colorbar; caxis([0 1]);
  title('Occupancy Grid Map (probability)');
  xlabel('x [m]'); ylabel('y [m]');

end

% ===== Helpers =====

function y = clamp(x, lo, hi)
  y = min(max(x, lo), hi);
end

function l = logit(p)
  p = clamp(p, 1e-9, 1 - 1e-9);
  l = log(p/(1-p));
end

function [gx,gy] = worldToGrid(spec, x, y)
  gx = floor((x - spec.ox) / spec.res);
  gy = floor((y - spec.oy) / spec.res);
end

function pts = bresenham(x0,y0,x1,y1)
  dx = abs(x1-x0);
  dy = abs(y1-y0);
  sx = 1; if x0 > x1, sx = -1; end
  sy = 1; if y0 > y1, sy = -1; end
  err = dx - dy;
  x = x0; y = y0;
  pts = [];
  while true
    pts = [pts; x y];
    if x==x1 && y==y1
      break;
    end
    e2 = 2*err;
    if e2 > -dy
      err = err - dy;
      x = x + sx;
    end
    if e2 < dx
      err = err + dx;
      y = y + sy;
    end
  end
end

function segs = buildBoxWorld(xmin, ymin, xmax, ymax)
  segs = {
    struct('x0',xmin,'y0',ymin,'x1',xmax,'y1',ymin);
    struct('x0',xmax,'y0',ymin,'x1',xmax,'y1',ymax);
    struct('x0',xmax,'y0',ymax,'x1',xmin,'y1',ymax);
    struct('x0',xmin,'y0',ymax,'x1',xmin,'y1',ymin);
  };
end

function t = raySegmentIntersection(rx, ry, rdx, rdy, s)
  sx = s.x1 - s.x0;
  sy = s.y1 - s.y0;
  rxs = rdx*sy - rdy*sx;
  if abs(rxs) < 1e-12
    t = NaN; return;
  end
  qpx = s.x0 - rx;
  qpy = s.y0 - ry;
  tCand = (qpx*sy - qpy*sx) / rxs;
  u = (qpx*rdy - qpy*rdx) / rxs;
  if tCand >= 0.0 && u >= 0.0 && u <= 1.0
    t = tCand;
  else
    t = NaN;
  end
end

function ranges = simulateLidarScan(pose, segs, angles, zMax)
  x = pose(1); y = pose(2); th = pose(3);
  ranges = zMax * ones(size(angles));
  for i = 1:length(angles)
    wa = th + angles(i);
    rdx = cos(wa); rdy = sin(wa);
    tmin = inf; any = false;
    for j = 1:length(segs)
      s = segs{j};
      t = raySegmentIntersection(x, y, rdx, rdy, s);
      if ~isnan(t) && t <= zMax && t < tmin
        tmin = t; any = true;
      end
    end
    if any
      ranges(i) = tmin;
    end
  end
end

function logOdds = updateFromScan(logOdds, spec, l0, lOcc, lFree, lMin, lMax, pose, angles, ranges, zMax)
  x = pose(1); y = pose(2); th = pose(3);
  [gx0, gy0] = worldToGrid(spec, x, y);

  for i = 1:length(angles)
    a = angles(i); r = ranges(i);
    wa = th + a;
    ex = x + r*cos(wa);
    ey = y + r*sin(wa);

    [gx1, gy1] = worldToGrid(spec, ex, ey);
    cells = bresenham(gx0, gy0, gx1, gy1);
    if isempty(cells), continue; end

    hit = (r < (zMax - 1e-12));
    if hit
      freeCells = cells(1:end-1,:);
    else
      freeCells = cells;
    end

    for k = 1:size(freeCells,1)
      gx = freeCells(k,1); gy = freeCells(k,2);
      if 0 <= gx && gx < spec.width && 0 <= gy && gy < spec.height
        logOdds(gy+1, gx+1) = clamp(logOdds(gy+1, gx+1) + (lFree - l0), lMin, lMax);
      end
    end

    if hit
      gx = cells(end,1); gy = cells(end,2);
      if 0 <= gx && gx < spec.width && 0 <= gy && gy < spec.height
        logOdds(gy+1, gx+1) = clamp(logOdds(gy+1, gx+1) + (lOcc - l0), lMin, lMax);
      end
    end
  end
end
      

6.5 Wolfram Mathematica

Chapter9_Lesson1.nb


(* Chapter9_Lesson1.nb (Wolfram Language script content)
   Occupancy Grid Mapping (2D) — Mathematica reference implementation.
   Generates a synthetic environment, ray-casts a simple LiDAR, and builds
   an occupancy grid with log-odds updates. Produces an ArrayPlot of P(occupied).
*)

ClearAll["Global`*"];

Clamp[x_, lo_, hi_] := Max[lo, Min[hi, x]];
Logit[p_] := Module[{pp = Clamp[p, 10^-9, 1 - 10^-9]}, Log[pp/(1 - pp)]];
Sigmoid[x_] := 1/(1 + Exp[-x]);

(* Grid specification *)
spec = <|
   "W" -> 220, "H" -> 180,
   "r" -> 0.1,
   "ox" -> -11.0, "oy" -> -9.0
|>;

p0 = 0.5; pOcc = 0.75; pFree = 0.30;
l0 = Logit[p0]; lOcc = Logit[pOcc]; lFree = Logit[pFree];
lMin = -8.0; lMax = 8.0;

logOdds = ConstantArray[l0, {spec["H"], spec["W"]}];

WorldToGrid[{x_, y_}] := Module[{gx, gy},
  gx = Floor[(x - spec["ox"])/spec["r"]];
  gy = Floor[(y - spec["oy"])/spec["r"]];
  {gx, gy}
];

InBoundsQ[{gx_, gy_}] := (0 <= gx < spec["W"] && 0 <= gy < spec["H"]);

UpdateCell[{gx_, gy_}, lUpdate_] := Module[{},
  If[!InBoundsQ[{gx, gy}], Return[]];
  logOdds[[gy + 1, gx + 1]] = Clamp[logOdds[[gy + 1, gx + 1]] + (lUpdate - l0), lMin, lMax];
];

(* Bresenham line traversal (inclusive endpoints) *)
Bresenham[{x0_, y0_}, {x1_, y1_}] := Module[
  {dx, dy, sx, sy, err, x = x0, y = y0, pts = {}, e2},
  dx = Abs[x1 - x0]; dy = Abs[y1 - y0];
  sx = If[x0 < x1, 1, -1];
  sy = If[y0 < y1, 1, -1];
  err = dx - dy;
  While[True,
    AppendTo[pts, {x, y}];
    If[x == x1 && y == y1, Break[]];
    e2 = 2 err;
    If[e2 > -dy, err = err - dy; x = x + sx;];
    If[e2 < dx, err = err + dx; y = y + sy;];
  ];
  pts
];

(* Environment as line segments: {x0,y0,x1,y1} *)
BuildBoxWorld[{xmin_, ymin_}, {xmax_, ymax_}] := {
  {xmin, ymin, xmax, ymin},
  {xmax, ymin, xmax, ymax},
  {xmax, ymax, xmin, ymax},
  {xmin, ymax, xmin, ymin}
};

segs = Join[
  BuildBoxWorld[{-10.0, -8.0}, {10.0, 8.0}],
  {
    {0.0, -6.0, 0.0, 6.0},
    {-8.0, 2.0, -2.0, 2.0}
  }
];

(* Ray/segment intersection using cross products. Return t or Missing[] *)
RaySegIntersect[{rx_, ry_}, {rdx_, rdy_}, {x0_, y0_, x1_, y1_}] := Module[
  {sx, sy, rxs, qpx, qpy, t, u},
  sx = x1 - x0; sy = y1 - y0;
  rxs = rdx sy - rdy sx;
  If[Abs[rxs] < 10^-12, Return[Missing["NoHit"]]];
  qpx = x0 - rx; qpy = y0 - ry;
  t = (qpx sy - qpy sx)/rxs;
  u = (qpx rdy - qpy rdx)/rxs;
  If[t >= 0 && 0 <= u <= 1, t, Missing["NoHit"]]
];

SimulateScan[{x_, y_, th_}, angles_List, zMax_] := Module[
  {ranges = ConstantArray[zMax, Length[angles]], wa, rdx, rdy, tlist, tmin},
  Do[
    wa = th + angles[[i]];
    rdx = Cos[wa]; rdy = Sin[wa];
    tlist = DeleteMissing[RaySegIntersect[{x, y}, {rdx, rdy}, #] & /@ segs];
    If[Length[tlist] > 0,
      tmin = Min[Select[tlist, # <= zMax &]];
      If[NumericQ[tmin], ranges[[i]] = tmin];
    ];
    ,
    {i, 1, Length[angles]}
  ];
  ranges
];

UpdateFromScan[{x_, y_, th_}, angles_List, ranges_List, zMax_] := Module[
  {g0, gx0, gy0, i, a, r, wa, ex, ey, g1, cells, hit, freeCells},
  g0 = WorldToGrid[{x, y}]; gx0 = g0[[1]]; gy0 = g0[[2]];
  Do[
    a = angles[[i]]; r = ranges[[i]];
    wa = th + a;
    ex = x + r Cos[wa];
    ey = y + r Sin[wa];
    g1 = WorldToGrid[{ex, ey}];
    cells = Bresenham[{gx0, gy0}, g1];
    If[Length[cells] == 0, Continue[]];
    hit = (r < (zMax - 10^-12));
    freeCells = If[hit, Most[cells], cells];
    (UpdateCell[#, lFree] & /@ freeCells);
    If[hit, UpdateCell[Last[cells], lOcc]];
    ,
    {i, 1, Length[angles]}
  ];
];

(* Trajectory *)
T = 120;
poses = Table[
  Module[{ang = 2 Pi (t - 1)/T},
    {-6.0 + 6.0 Cos[ang], 0.0 + 4.0 Sin[ang], ang + Pi/2}
  ],
  {t, 1, T}
];

fov = Pi; nBeams = 181;
angles = Table[-fov/2 + fov (i - 1)/(nBeams - 1), {i, 1, nBeams}];
zMax = 10.0;

SeedRandom[0];
Do[
  Module[{pose = poses[[t]], scan},
    scan = SimulateScan[pose, angles, zMax];
    scan = Clamp[scan + RandomVariate[NormalDistribution[0, 0.02], Length[scan]], 0.0, zMax];
    UpdateFromScan[pose, angles, scan, zMax];
  ],
  {t, 1, T}
];

pOccGrid = Sigmoid /@ logOdds;

ArrayPlot[
  pOccGrid,
  ColorFunction -> "GrayTones",
  PlotRange -> {0, 1},
  Frame -> True,
  FrameLabel -> {"gx", "gy"},
  PlotLabel -> "Occupancy Grid Map (P(occupied))"
]
      

7. Problems and Solutions

Problem 1 (Grid sizing and memory): A robot must map a rectangular area of size \( L_x = 30\,\mathrm{m} \) by \( L_y = 20\,\mathrm{m} \). You choose resolution \( r = 0.05\,\mathrm{m} \) per cell. (a) Compute \( W, H, N \). (b) If you store one double-precision log-odds per cell (8 bytes), estimate the memory in MB.

Solution: (a) \( W = \lfloor L_x/r \rfloor = \lfloor 30/0.05 \rfloor = 600 \), \( H = \lfloor 20/0.05 \rfloor = 400 \), so \( N = WH = 240{,}000 \) cells. (b) Memory is \( 8N \) bytes \( = 1{,}920{,}000 \) bytes \( \approx 1.83\,\mathrm{MB} \).

Problem 2 (Derive the odds update): Starting from the per-cell Bayesian update \( p(m_i\mid x_{1:t}, z_{1:t}) = \eta\, \frac{p(m_i\mid z_t, x_t)}{p(m_i)}\, p(m_i\mid x_{1:t-1}, z_{1:t-1}) \), derive the odds update \( O_t(i) = O_{t-1}(i)\, \frac{O_{inv}(i)}{O_0(i)} \).

Solution: Write the update once for \( m_i=1 \) and once for \( m_i=0 \). Divide the two equations. The normalizer \( \eta \) cancels, leaving:

\[ \frac{p(m_i=1\mid x_{1:t}, z_{1:t})}{p(m_i=0\mid x_{1:t}, z_{1:t})} = \frac{p(m_i=1\mid x_{1:t-1}, z_{1:t-1})}{p(m_i=0\mid x_{1:t-1}, z_{1:t-1})} \cdot \frac{\frac{p(m_i=1\mid z_t, x_t)}{p(m_i=1)}}{\frac{p(m_i=0\mid z_t, x_t)}{p(m_i=0)}}. \]

Recognize the left side as \( O_t(i) \), the first ratio on the right as \( O_{t-1}(i) \), the numerator odds as \( O_{inv}(i) \), and the denominator odds as \( O_0(i) \).

Problem 3 (Max-range returns): In the inverse sensor model, why do we typically avoid marking an “occupied cell” when the measurement equals the max range \( z_t^{(k)} = z_{max} \)?

Solution: A max-range return often indicates “no obstacle detected within sensor range.” Marking an occupied endpoint would create false obstacles at the sensor horizon, biasing the map toward spurious walls. In the piecewise model of Section 4, the occupied case requires \( z_t^{(k)} < z_{max} \).

Problem 4 (Closed-form probability after repeated identical evidence): Suppose a cell starts with prior \( p_0 \). At each time step the inverse model returns a constant \( p_{inv} \) for that cell (e.g., repeated “free” evidence with \( p_{inv} < 0.5 \)). Assuming independence and using the odds update repeatedly, derive \( p_t \) after \( t \) updates.

Solution: Let \( O_0 = \frac{p_0}{1-p_0} \) and \( O_{inv} = \frac{p_{inv}}{1-p_{inv}} \). Repeating \( O_t = O_{t-1} \frac{O_{inv}}{O_0} \) yields \( O_t = O_0 \left(\frac{O_{inv}}{O_0}\right)^t \). Convert back to probability:

\[ p_t = \frac{O_t}{1+O_t} = \frac{O_0 \left(\frac{O_{inv}}{O_0}\right)^t}{1 + O_0 \left(\frac{O_{inv}}{O_0}\right)^t}. \]

If \( p_{inv} < 0.5 \), then \( O_{inv} < 1 \) and the odds shrink with \( t \), driving \( p_t \) toward 0 (free).

Problem 5 (Inverse model design): Suppose you choose \( p_{occ}=0.6 \) and \( p_{free}=0.4 \). Explain qualitatively how the map will differ from the choice \( p_{occ}=0.9 \), \( p_{free}=0.1 \) after many updates in a static world.

Solution: Using log-odds increments, the “strength” of each update is \( \Delta\ell_{occ} = \operatorname{logit}(p_{occ})-\operatorname{logit}(p_0) \) and \( \Delta\ell_{free} = \operatorname{logit}(p_{free})-\operatorname{logit}(p_0) \). With \( p_{occ}=0.6 \) and \( p_{free}=0.4 \), the increments have small magnitude, so the map becomes conservative and “uncertain” (probabilities remain closer to 0.5). With \( p_{occ}=0.9 \) and \( p_{free}=0.1 \), increments have large magnitude, so the map saturates quickly (close to 0 or 1) and becomes overconfident under any model mismatch; clamping becomes more important.

8. Summary

We defined occupancy grids as Bernoulli random fields over a discretized workspace, formulated mapping as inference of \( p(m\mid x_{1:t}, z_{1:t}) \), and derived per-cell Bayesian updates using inverse sensor models. We motivated odds/log-odds representations for stable incremental updates and connected the update logic to practical ray traversal. Lesson 2 formalizes log-odds updates and Bayesian cell updates in greater detail and emphasizes efficient implementation patterns.

9. References

  1. Moravec, H.P. (1988). Sensor fusion in certainty grids for mobile robots. AI Magazine, 9(2), 61–74.
  2. Elfes, A. (1989). Using occupancy grids for mobile robot perception and navigation. Computer, 22(6), 46–57.
  3. Thrun, S. (2003). Learning occupancy grid maps with forward sensor models. Autonomous Robots, 15(2), 111–127.
  4. Hornung, A., Wurm, K.M., Bennewitz, M., Stachniss, C., & Burgard, W. (2013). OctoMap: An efficient probabilistic 3D mapping framework based on octrees. Autonomous Robots, 34(3), 189–206.
  5. Durrant-Whyte, H., & Bailey, T. (2006). Simultaneous localization and mapping: Part I. IEEE Robotics & Automation Magazine, 13(2), 99–110.
  6. Bailey, T., & Durrant-Whyte, H. (2006). Simultaneous localization and mapping: Part II. IEEE Robotics & Automation Magazine, 13(3), 108–117.
  7. Coué, C., Pradalier, C., Laugier, C., Fraichard, T., & Bessière, P. (2006). Bayesian occupancy filtering for multi-target tracking: An automotive application. International Journal of Robotics Research, 25(1), 19–30.
  8. Nègre, A., Rummelhard, L., & Laugier, C. (2014). Hybrid sampling Bayesian occupancy filter. Autonomous Robots, 37(1), 55–70.