Chapter 17: Exploration and Active Mapping

Lesson 4: Exploration Under Limited Battery/Time

This lesson formalizes budget-aware exploration where an AMR must reduce map uncertainty while respecting hard limits on remaining battery energy and mission time, and still guaranteeing a safe return (or safe stop). We build a constrained objective using information gain, derive practical selection rules via Lagrangian relaxation, and connect exploration action selection to submodular maximization under knapsack-like budgets. The emphasis is on implementable receding-horizon algorithms that fit within the standard sensing–mapping–planning loop from earlier lessons.

1. Conceptual Overview

In Chapter 17 so far, we treated exploration as choosing actions (frontiers / viewpoints) to maximize \( \Delta \)-entropy reduction or other information objectives. Real AMRs also face budgets:

  • Energy budget (battery): traction power, compute/sensing power, plus safety reserve.
  • Time budget: mission deadline, time-to-return, or “must dock by time \(T\)”.

We will model exploration as choosing a sequence of decision points \( \pi = (a_1,a_2,\dots,a_N) \) (e.g., next-best-views), where each action yields expected information gain but consumes energy and time. The key constraint is that exploration must remain feasible: the robot must be able to reach the next goal and still return to a terminal set (home/dock/safe zone) with a reserve.

flowchart TD
  S["Start: belief map + budgets"] --> O["Observe and update map"]
  O --> F["Compute candidate targets (frontiers / views)"]
  F --> E["Estimate utility: IG - lambda*cost"]
  E --> C["Check feasibility: can reach target and return with reserve?"]
  C -->|yes| G["Choose best feasible target"]
  C -->|no| R["Switch to return-to-home policy"]
  G --> M["Move + local avoidance (previous chapters)"]
  M --> O
  R --> H["Navigate to home/dock"]
  H --> X["Stop/Recharge/End"]
        

The loop above is a receding-horizon policy: we plan one target at a time using the current belief and remaining budgets. This fits the AMR pipeline: mapping uncertainty and reachable space change as the robot senses and discovers obstacles.

2. Battery and Time Models for Exploration

Let remaining battery energy be \( E(t) \) (Joules) and remaining mission time be \( T(t) \) (seconds). A minimal continuous model is:

\[ \dot{E}(t) = -P(t), \qquad \dot{T}(t) = -1, \qquad E(t)\ge 0,\; T(t)\ge 0 \]

The total power splits into traction and base loads: \( P(t)=P_{\text{drive} }(v(t),\kappa(t),\text{terrain}) + P_{\text{base} } \), where \( v(t) \) is speed and \( \kappa(t) \) curvature (related to turning losses and slip).

For waypoint-to-waypoint planning, we usually discretize into motion segments \( i=1,\dots,N \). If segment \( i \) has length \( \ell_i \) and average speed \( v_i \), then time \( \Delta t_i = \ell_i/v_i \) and energy is approximately:

\[ \Delta E_i \approx \left(P_{\text{drive} }(v_i) + P_{\text{base} }\right)\Delta t_i = \left(P_{\text{drive} }(v_i) + P_{\text{base} }\right)\frac{\ell_i}{v_i} \]

In many indoor AMR stacks, a practical abstraction is cost-per-meter (or cost-per-grid-step):

\[ \Delta E_i \approx c_E \,\ell_i + c_0\,\Delta t_i, \qquad \Delta T_i = \Delta t_i \]

where \( c_E \) captures traction cost per distance and \( c_0 \) captures base loads. This lesson focuses on decision-making given such costs, not on electrochemistry.

Safety reserve. We enforce a reserve energy \( E_{\text{res} } \) so the robot never plans down to zero: \( E(t)\ge E_{\text{res} } \). Reserve accounts for localization failures, detours, and braking/stopping.

3. Constrained Objective: Information Gain Under Budgets

Let \( \mathcal{M} \) be the map random variable (occupancy grid or features). The belief at time \( t \) has entropy \( H(\mathcal{M}\mid \mathcal{D}_t) \) where \( \mathcal{D}_t \) denotes collected sensor data. For a candidate action/goal \( a \) (e.g., a viewpoint), define its expected information gain:

\[ \operatorname{IG}(a \mid \mathcal{D}_t) = \mathbb{E}\!\left[ H(\mathcal{M}\mid \mathcal{D}_t) - H(\mathcal{M}\mid \mathcal{D}_t \cup Z(a)) \right] \]

where \( Z(a) \) is the random future observation obtained after executing action \( a \). If we choose a sequence (policy) \( \pi \), then the “total gain” is \( \sum_k \operatorname{IG}(a_k\mid \mathcal{D}_{t_k}) \), but note that gains are computed under changing beliefs.

A standard constrained formulation is:

\[ \max_{\pi}\;\; \mathbb{E}\!\left[\sum_{k=1}^{N} \operatorname{IG}(a_k \mid \mathcal{D}_{t_k})\right] \quad \text{s.t.}\quad \sum_{k=1}^{N} C_E(a_k) \le B_E,\;\; \sum_{k=1}^{N} C_T(a_k) \le B_T \]

where \( C_E(a) \) is expected energy cost (including travel and sensing) and \( C_T(a) \) is time cost. In practice, we also impose a terminal feasibility condition (Section 5): each chosen goal must still allow return-to-home with reserve.

Lagrangian relaxation (practical selection rule). Introduce multipliers \( \lambda_E \ge 0 \) and \( \lambda_T \ge 0 \) and optimize the relaxed objective:

\[ \max_{\pi}\;\; \mathbb{E}\!\left[\sum_{k=1}^{N} \Big(\operatorname{IG}(a_k\mid \mathcal{D}_{t_k}) - \lambda_E C_E(a_k) - \lambda_T C_T(a_k)\Big)\right] \]

This converts “hard budgets” into a weighted trade-off, which is exactly what many real-time explorers do: choose the action with largest \( \operatorname{IG} - \lambda \times \text{cost} \) among feasible candidates, and update budgets after execution. The multipliers can be fixed (engineering choice) or adapted online (Section 6).

4. Why Greedy Often Works: Submodular Gains Under a Budget

When viewpoints overlap in what they can observe, information gain exhibits diminishing returns: the more you have already observed, the less a new nearby viewpoint adds. This is captured by submodularity.

Consider a simplified setting where each candidate viewpoint \( a \) reveals a set of map cells (or features) \( \mathcal{V}(a) \). Define a “coverage gain” set function:

\[ f(S) = \left|\bigcup_{a\in S}\mathcal{V}(a)\right| \]

Claim (submodularity of coverage). For any \( A \subseteq B \) and any action \( a \notin B \), the marginal gain satisfies: \( f(A\cup\{a\}) - f(A) \ge f(B\cup\{a\}) - f(B) \). Intuitively, if you already cover more (set \( B \)), adding \( a \) yields fewer new cells.

With costs \( c(a) > 0 \) and budget \( \sum_{a\in S} c(a)\le B \) (a knapsack constraint), the exact optimum is generally NP-hard. However, a classic result shows that greedy selection can be near-optimal for monotone submodular objectives.

Theorem (classical approximation, sketch). If \( f \) is monotone submodular and \( f(\emptyset)=0 \), then a greedy algorithm that repeatedly picks the item with maximum “marginal gain per cost” achieves a constant-factor approximation (commonly stated near \( 1-1/e \) with appropriate variants and bookkeeping).

Sketch of the key inequality (diminishing returns argument). Let \( S_k \) be greedy set after cost \( C_k \). Under submodularity, the best remaining marginal gain per unit cost is at least the average remaining gain of an optimal set normalized by remaining budget, implying:

\[ f(S_{k+1}) - f(S_k) \ge \frac{c(a_{k+1})}{B}\Big(f(S^\star) - f(S_k)\Big) \]

Rearranging and iterating yields an exponential decay of the gap \( f(S^\star)-f(S_k) \), leading to the familiar \( 1-e^{-C_k/B} \) style bound, and therefore a constant-factor guarantee when \( C_k \) approaches \( B \). In exploration, this theory justifies a greedy “value-per-cost” rule for selecting viewpoints/frontiers.

5. Terminal Feasibility and the “Must-Return” Constraint

Unlike pure coverage objectives, real AMRs must ensure they can reach a terminal set (dock/home/safe zone) from wherever they decide to go. Let \( x \) be current pose, \( g \) a candidate target (frontier/view), and \( h \) be home/dock. Let \( d(x,y) \) be shortest-path distance on the current traversability map. With an energy-per-meter model \( c_E \) and base term \( c_0 \), define the travel energy estimate:

\[ \widehat{C}_E(x \rightarrow y) \approx c_E\, d(x,y) + c_0\,\widehat{t}(x,y) \]

The return-feasibility constraint for choosing target \( g \) is:

\[ E_{\text{rem} } - \widehat{C}_E(x \rightarrow g) - \widehat{C}_E(g \rightarrow h) \ge E_{\text{res} } \]

Similarly for time:

\[ T_{\text{rem} } - \widehat{C}_T(x \rightarrow g) - \widehat{C}_T(g \rightarrow h) \ge 0 \]

Important engineering note. Because the map is uncertain, \( d(\cdot,\cdot) \) is itself uncertain. Robust planners use inflated costs (risk-aware) or conservative bounds (e.g., add a detour factor \( \alpha > 1 \)): \( \widehat{C}_E \leftarrow \alpha\,\widehat{C}_E \).

stateDiagram-v2
  [*] --> Explore
  Explore --> Explore: "feasible target exists"
  Explore --> Return: "no feasible target"
  Explore --> Return: "budget near threshold"
  Return --> Dock: "arrive home/dock"
  Dock --> [*]
        

The state structure above is minimal but critical: exploration is always subordinate to safety and mission constraints.

6. Choosing the Cost Weights Online

In Section 3 we introduced Lagrangian weights \( \lambda_E,\lambda_T \). A fixed choice works, but we can also adapt them based on remaining budgets. A simple proportional rule is:

\[ \lambda_E(t) = \lambda_{E,0}\left(\frac{E_{\max} }{E_{\text{rem} }(t)}\right), \qquad \lambda_T(t) = \lambda_{T,0}\left(\frac{T_{\max} }{T_{\text{rem} }(t)}\right) \]

As budgets shrink, the multipliers grow, discouraging long detours for small gains. More principled approaches use dual ascent (treating \( \lambda \) as dual variables) by nudging them upward whenever measured consumption violates planned constraints.

Receding-horizon target selection. Let candidate set be \( \mathcal{G}_t \) (frontiers / viewpoints). For each \( g\in\mathcal{G}_t \) we compute:

\[ U(g) = \operatorname{IG}(g\mid \mathcal{D}_t) - \lambda_E(t)\widehat{C}_E(x\rightarrow g) - \lambda_T(t)\widehat{C}_T(x\rightarrow g) \]

and choose \( g^\star = \arg\max U(g) \) among those satisfying the return-feasibility inequalities from Section 5. Then execute navigation to \( g^\star \) using the already-taught local planners (DWA/TEB/etc.), update the map, and repeat.

This is the core “budget-aware exploration” pattern implemented in the code labs below.

7. Python Implementation (Budget-Aware Frontier Exploration)

This implementation is self-contained (no ROS). In a ROS2 system, you would replace:

  • Map belief updates with OccupancyGrid / OctoMap updates (Chapter 9).
  • Frontier extraction with your exploration package or custom frontier node.
  • Travel cost estimates with Nav2 global planner path length.
  • Execution with action clients to Nav2 (NavigateToPose).

Code: Chapter17_Lesson4.py


# Chapter17_Lesson4.py
# Exploration Under Limited Battery/Time (Budget-Aware Frontier Exploration)
# Author: course material generator
#
# This script demonstrates a simple, self-contained budget-aware exploration loop:
# - A probabilistic occupancy grid with unknown cells
# - Frontier detection
# - Utility = expected information gain - lambda * travel_cost
# - Safety constraint: must have enough budget to reach goal and return home + reserve
#
# No ROS required; "integration points" are noted in comments.

from __future__ import annotations
from dataclasses import dataclass
from typing import List, Tuple, Optional, Dict
import heapq
import math
import random

random.seed(7)

Cell = Tuple[int, int]

def clamp(x: float, lo: float, hi: float) -> float:
    return max(lo, min(hi, x))

def entropy_bern(p: float) -> float:
    """Binary entropy H(p) in nats."""
    p = clamp(p, 1e-9, 1 - 1e-9)
    return -(p * math.log(p) + (1 - p) * math.log(1 - p))

@dataclass
class Budget:
    # energy in Joules (or arbitrary units)
    E: float
    # time in seconds (or arbitrary units)
    T: float

@dataclass
class RobotParams:
    # cost per grid step (move energy) and per step time
    E_step: float = 1.0
    T_step: float = 1.0
    # idle/base costs per step
    E_base: float = 0.10
    # reserve to guarantee safe stop/return
    E_reserve: float = 10.0

@dataclass
class SensorParams:
    # sensing radius (Chebyshev radius in grid)
    r: int = 3
    # observation quality: new occupancy probability when observed obstacle
    p_occ_obs: float = 0.95
    p_free_obs: float = 0.05

class OccupancyGrid:
    """
    Probability grid p_occ in [0,1].
    Unknown cells are initialized to 0.5.
    """
    def __init__(self, w: int, h: int):
        self.w, self.h = w, h
        self.p = [[0.5 for _ in range(w)] for _ in range(h)]

    def inb(self, c: Cell) -> bool:
        x, y = c
        return 0 <= x < self.w and 0 <= y < self.h

    def get(self, c: Cell) -> float:
        x, y = c
        return self.p[y][x]

    def set(self, c: Cell, p_occ: float) -> None:
        x, y = c
        self.p[y][x] = clamp(p_occ, 0.0, 1.0)

    def is_free(self, c: Cell, thr: float = 0.65) -> bool:
        return self.get(c) < thr

    def is_occupied(self, c: Cell, thr: float = 0.65) -> bool:
        return self.get(c) > thr

    def neighbors4(self, c: Cell) -> List[Cell]:
        x, y = c
        cand = [(x+1,y),(x-1,y),(x,y+1),(x,y-1)]
        return [q for q in cand if self.inb(q)]

def astar(grid: OccupancyGrid, s: Cell, g: Cell, occ_thr: float = 0.65) -> Optional[List[Cell]]:
    """A* on 4-neighborhood; treats p_occ > occ_thr as blocked."""
    if not grid.inb(s) or not grid.inb(g):
        return None
    if not grid.is_free(s, occ_thr) or not grid.is_free(g, occ_thr):
        return None

    def h(c: Cell) -> float:
        return abs(c[0]-g[0]) + abs(c[1]-g[1])

    openpq: List[Tuple[float, Cell]] = []
    heapq.heappush(openpq, (h(s), s))
    gscore: Dict[Cell, float] = {s: 0.0}
    parent: Dict[Cell, Cell] = {}

    while openpq:
        _, cur = heapq.heappop(openpq)
        if cur == g:
            # reconstruct
            path = [cur]
            while cur in parent:
                cur = parent[cur]
                path.append(cur)
            path.reverse()
            return path

        for nb in grid.neighbors4(cur):
            if not grid.is_free(nb, occ_thr):
                continue
            tentative = gscore[cur] + 1.0
            if nb not in gscore or tentative < gscore[nb]:
                gscore[nb] = tentative
                parent[nb] = cur
                heapq.heappush(openpq, (tentative + h(nb), nb))
    return None

def detect_frontiers(grid: OccupancyGrid, free_thr: float = 0.35) -> List[Cell]:
    """
    A frontier cell: free (p_occ < free_thr) and has a 4-neighbor unknown-ish (close to 0.5).
    """
    frontiers = []
    for y in range(grid.h):
        for x in range(grid.w):
            c = (x, y)
            p = grid.get(c)
            if p >= free_thr:
                continue
            # unknown neighbor?
            for nb in grid.neighbors4(c):
                pnb = grid.get(nb)
                if abs(pnb - 0.5) < 0.15:
                    frontiers.append(c)
                    break
    return frontiers

def expected_info_gain(grid: OccupancyGrid, pose: Cell, sensor: SensorParams) -> float:
    """
    Approximate IG as entropy reduction over cells in sensor range, assuming observation makes
    p_occ closer to {p_occ_obs or p_free_obs}. In a real system, this would integrate
    measurement model over rays (LiDAR) or pixels (vision).
    """
    x0, y0 = pose
    ig = 0.0
    for dy in range(-sensor.r, sensor.r+1):
        for dx in range(-sensor.r, sensor.r+1):
            c = (x0+dx, y0+dy)
            if not grid.inb(c):
                continue
            p0 = grid.get(c)
            H0 = entropy_bern(p0)
            # expected posterior entropy (simple symmetric mixture proxy)
            # posterior becomes more "certain" than prior
            p_post1 = clamp(sensor.p_occ_obs, 0.0, 1.0)
            p_post0 = clamp(sensor.p_free_obs, 0.0, 1.0)
            H_post = 0.5 * entropy_bern(p_post1) + 0.5 * entropy_bern(p_post0)
            # only count if cell is not already certain
            w_unc = clamp(1.0 - abs(p0 - 0.5) * 2.0, 0.0, 1.0)
            ig += w_unc * max(0.0, H0 - H_post)
    return ig

def apply_observation(grid: OccupancyGrid, truth: OccupancyGrid, pose: Cell, sensor: SensorParams) -> None:
    """Simulate observation by revealing truth in sensor neighborhood with noisy posterior."""
    x0, y0 = pose
    for dy in range(-sensor.r, sensor.r+1):
        for dx in range(-sensor.r, sensor.r+1):
            c = (x0+dx, y0+dy)
            if not grid.inb(c):
                continue
            # truth is binary obstacle vs free by thresholding its probability
            is_occ = truth.is_occupied(c, thr=0.5)
            grid.set(c, sensor.p_occ_obs if is_occ else sensor.p_free_obs)

def pick_goal_budgeted(
    grid: OccupancyGrid,
    frontiers: List[Cell],
    cur: Cell,
    home: Cell,
    budget: Budget,
    rp: RobotParams,
    sensor: SensorParams,
    lam: float = 0.25,
    occ_thr: float = 0.65,
    max_candidates: int = 80
) -> Optional[Tuple[Cell, List[Cell], float]]:
    """
    Choose frontier maximizing U = IG(goal) - lam * travel_cost
    subject to energy/time feasibility INCLUDING return-to-home.
    """
    if not frontiers:
        return None

    # subsample for speed
    if len(frontiers) > max_candidates:
        frontiers = random.sample(frontiers, max_candidates)

    best = None
    bestU = -1e18
    best_path = None

    for g in frontiers:
        path = astar(grid, cur, g, occ_thr=occ_thr)
        if path is None:
            continue
        # cost to reach g
        steps = max(0, len(path) - 1)
        E_go = steps * (rp.E_step + rp.E_base)
        T_go = steps * rp.T_step

        # return cost (shortest path from g to home)
        path_back = astar(grid, g, home, occ_thr=occ_thr)
        if path_back is None:
            # if we cannot guarantee return on current map, treat as infeasible
            continue
        steps_back = max(0, len(path_back) - 1)
        E_back = steps_back * (rp.E_step + rp.E_base)
        T_back = steps_back * rp.T_step

        # feasibility with reserve
        if budget.E - (E_go + E_back) < rp.E_reserve:
            continue
        if budget.T - (T_go + T_back) < 0:
            continue

        ig = expected_info_gain(grid, g, sensor)
        U = ig - lam * (steps)  # travel cost proxy
        if U > bestU:
            bestU = U
            best = g
            best_path = path

    if best is None:
        return None
    return best, best_path, bestU

def run_demo():
    # grid and truth
    W, H = 35, 25
    belief = OccupancyGrid(W, H)
    truth = OccupancyGrid(W, H)

    # create synthetic obstacles in truth (maze-ish)
    for y in range(H):
        for x in range(W):
            truth.set((x,y), 0.05)  # mostly free

    # borders
    for x in range(W):
        truth.set((x,0), 0.95); truth.set((x,H-1), 0.95)
    for y in range(H):
        truth.set((0,y), 0.95); truth.set((W-1,y), 0.95)

    # random rectangles
    rng = random.Random(3)
    for _ in range(8):
        x0 = rng.randint(3, W-10)
        y0 = rng.randint(3, H-8)
        ww = rng.randint(3, 7)
        hh = rng.randint(2, 5)
        for y in range(y0, min(H-1, y0+hh)):
            for x in range(x0, min(W-1, x0+ww)):
                truth.set((x,y), 0.95)

    # start/home
    home = (2, 2)
    cur = home

    rp = RobotParams(E_step=1.2, T_step=1.0, E_base=0.10, E_reserve=15.0)
    sensor = SensorParams(r=3)

    # budgets
    budget = Budget(E=160.0, T=140.0)

    # initial observation
    apply_observation(belief, truth, cur, sensor)

    explored_steps = 0
    goals_reached = 0

    while True:
        frontiers = detect_frontiers(belief)
        if not frontiers:
            print("No frontiers left. Exploration complete.")
            break

        choice = pick_goal_budgeted(
            belief, frontiers, cur, home, budget, rp, sensor,
            lam=0.35, occ_thr=0.65
        )
        if choice is None:
            print("No feasible frontier under remaining budget. Returning home.")
            break

        goal, path, U = choice
        # execute path step-by-step, update budgets and sensing
        for step in path[1:]:
            # consume
            budget.E -= (rp.E_step + rp.E_base)
            budget.T -= rp.T_step
            explored_steps += 1
            cur = step
            apply_observation(belief, truth, cur, sensor)

            if budget.E < rp.E_reserve or budget.T <= 0:
                print("Budget exhausted mid-path. Returning home (if possible).")
                break
        goals_reached += 1

        # stop if too low to continue safely
        if budget.E < rp.E_reserve or budget.T <= 0:
            break

        if goals_reached >= 40:
            print("Stop: reached demo goal limit.")
            break

    # return home (best effort)
    back = astar(belief, cur, home, occ_thr=0.65)
    if back is not None:
        for step in back[1:]:
            budget.E -= (rp.E_step + rp.E_base)
            budget.T -= rp.T_step
            cur = step

    print(f"Final pose: {cur}, steps: {explored_steps}, goals: {goals_reached}")
    print(f"Remaining budget: E={budget.E:.2f}, T={budget.T:.2f}")

if __name__ == "__main__":
    run_demo()
      

Exercise Code: Chapter17_Lesson4_Ex1.py


# Chapter17_Lesson4_Ex1.py
# Exercise: Greedy knapsack selection for monotone submodular set function
# We illustrate the (1-1/e) style idea using a toy coverage objective.

from __future__ import annotations
from typing import List, Set, Tuple
import math

def greedy_submodular_knapsack(
    items: List[Tuple[str, Set[int], float]],
    B: float
) -> List[str]:
    """
    items: list of (name, covered_elements, cost)
    objective: f(S) = |union covered|
    choose S with sum(cost) <= B maximizing f(S)
    greedy by marginal gain per cost.
    """
    chosen = []
    covered: Set[int] = set()
    remaining = items[:]
    budget = B

    while True:
        best = None
        best_ratio = -1.0
        for name, elems, cost in remaining:
            if cost > budget:
                continue
            marg = len((covered | elems)) - len(covered)
            ratio = marg / cost if cost > 0 else float("inf")
            if ratio > best_ratio and marg > 0:
                best_ratio = ratio
                best = (name, elems, cost)

        if best is None:
            break

        name, elems, cost = best
        chosen.append(name)
        covered |= elems
        budget -= cost
        remaining = [it for it in remaining if it[0] != name]

    return chosen

if __name__ == "__main__":
    # toy environment elements 0..19 to "cover"
    items = [
        ("A", {0,1,2,3,4,5}, 3.0),
        ("B", {4,5,6,7,8}, 2.0),
        ("C", {9,10,11,12}, 2.5),
        ("D", {12,13,14,15,16}, 3.0),
        ("E", {16,17,18,19}, 1.8),
        ("F", {2,3,8,9,17}, 2.2),
    ]
    B = 7.0
    S = greedy_submodular_knapsack(items, B)
    print("Budget:", B)
    print("Chosen:", S)
      

8. C++ Implementation (Same Logic, No ROS Dependency)

In ROS2 C++, the same selection loop would live in a node that subscribes to the map and publishes goals. Relevant libraries: rclcpp, Nav2 costmap interfaces, and TF2 for transforms. The code below remains standalone for learning clarity.

Code: Chapter17_Lesson4.cpp


// Chapter17_Lesson4.cpp
// Exploration Under Limited Battery/Time (Budget-Aware Frontier Exploration)
// Self-contained demo without ROS.
// Build: g++ -O2 -std=c++17 Chapter17_Lesson4.cpp -o demo
#include <bits/stdc++.h>
using namespace std;

struct Budget { double E, T; };
struct RobotParams { double E_step=1.2, T_step=1.0, E_base=0.10, E_reserve=15.0; };
struct SensorParams { int r=3; };

static inline double clampd(double x,double lo,double hi){ return max(lo,min(hi,x)); }

static double entropyBern(double p){
    p = clampd(p, 1e-9, 1.0-1e-9);
    return -(p*log(p) + (1.0-p)*log(1.0-p));
}

struct Grid {
    int W,H;
    vector<double> p; // row-major p_occ
    Grid(int w,int h):W(w),H(h),p(w*h,0.5){}
    bool inb(int x,int y) const { return 0<=x && x<W && 0<=y && y<H; }
    double get(int x,int y) const { return p[y*W+x]; }
    void setv(int x,int y,double v){ p[y*W+x]=clampd(v,0.0,1.0); }
    bool isFree(int x,int y,double thr=0.65) const { return get(x,y) < thr; }
    bool isOcc(int x,int y,double thr=0.65) const { return get(x,y) > thr; }
};

using Cell = pair<int,int>;

static vector<Cell> neigh4(const Grid& g, Cell c){
    int x=c.first, y=c.second;
    vector<Cell> cand={ {x+1,y},{x-1,y},{x,y+1},{x,y-1} };
    vector<Cell> out;
    for(auto &q:cand) if(g.inb(q.first,q.second)) out.push_back(q);
    return out;
}

static vector<Cell> astar(const Grid& g, Cell s, Cell goal, double occ_thr=0.65){
    if(!g.inb(s.first,s.second) || !g.inb(goal.first,goal.second)) return {};
    if(!g.isFree(s.first,s.second,occ_thr) || !g.isFree(goal.first,goal.second,occ_thr)) return {};
    auto h = [&](Cell c){ return abs(c.first-goal.first)+abs(c.second-goal.second); };

    unordered_map<long long,double> gscore;
    unordered_map<long long, long long> parent;
    auto key=[&](Cell c)->long long{ return (long long)c.second*g.W + c.first; };

    struct Node{ double f; Cell c; };
    struct Cmp{ bool operator()(const Node& a,const Node& b) const { return a.f>b.f; } };
    priority_queue<Node, vector<Node>, Cmp> pq;
    pq.push({(double)h(s), s});
    gscore[key(s)] = 0.0;

    while(!pq.empty()){
        auto cur = pq.top().c; pq.pop();
        if(cur==goal){
            vector<Cell> path; path.push_back(cur);
            long long k=key(cur);
            while(parent.count(k)){
                k = parent[k];
                path.push_back({(int)(k%g.W), (int)(k/g.W)});
            }
            reverse(path.begin(), path.end());
            return path;
        }
        long long kc=key(cur);
        double gc=gscore[kc];
        for(auto nb: neigh4(g,cur)){
            if(!g.isFree(nb.first, nb.second, occ_thr)) continue;
            long long kn=key(nb);
            double tent = gc + 1.0;
            if(!gscore.count(kn) || tent < gscore[kn]){
                gscore[kn]=tent;
                parent[kn]=kc;
                pq.push({tent + (double)h(nb), nb});
            }
        }
    }
    return {};
}

static vector<Cell> detectFrontiers(const Grid& g, double free_thr=0.35){
    vector<Cell> fr;
    for(int y=0;y<g.H;y++){
        for(int x=0;x<g.W;x++){
            double p=g.get(x,y);
            if(p >= free_thr) continue;
            for(auto nb: neigh4(g,{x,y})){
                double pn=g.get(nb.first, nb.second);
                if(fabs(pn-0.5) < 0.15){ fr.push_back({x,y}); break; }
            }
        }
    }
    return fr;
}

static double expectedIG(const Grid& belief, Cell pose, const SensorParams& sp){
    double ig=0.0;
    for(int dy=-sp.r; dy<=sp.r; dy++){
        for(int dx=-sp.r; dx<=sp.r; dx++){
            int x=pose.first+dx, y=pose.second+dy;
            if(!belief.inb(x,y)) continue;
            double p0 = belief.get(x,y);
            double H0 = entropyBern(p0);
            // simple proxy posterior entropy
            double Hpost = 0.5*entropyBern(0.95) + 0.5*entropyBern(0.05);
            double w = clampd(1.0 - fabs(p0-0.5)*2.0, 0.0, 1.0);
            ig += w * max(0.0, H0 - Hpost);
        }
    }
    return ig;
}

static void applyObs(Grid& belief, const Grid& truth, Cell pose, const SensorParams& sp){
    for(int dy=-sp.r; dy<=sp.r; dy++){
        for(int dx=-sp.r; dx<=sp.r; dx++){
            int x=pose.first+dx, y=pose.second+dy;
            if(!belief.inb(x,y)) continue;
            bool isOcc = truth.isOcc(x,y,0.5);
            belief.setv(x,y, isOcc ? 0.95 : 0.05);
        }
    }
}

static bool pickGoalBudgeted(
    const Grid& belief,
    const vector<Cell>& frontiers,
    Cell cur, Cell home,
    const Budget& budget,
    const RobotParams& rp,
    const SensorParams& sp,
    double lam,
    Cell& bestGoal,
    vector<Cell>& bestPath
){
    double bestU = -1e18;
    bool found=false;
    // subsample if huge
    vector<Cell> cand=frontiers;
    if((int)cand.size()>80){
        std::mt19937 rng(7);
        shuffle(cand.begin(), cand.end(), rng);
        cand.resize(80);
    }

    for(auto g: cand){
        auto path = astar(belief, cur, g, 0.65);
        if(path.empty()) continue;
        int steps = max(0, (int)path.size()-1);
        double Ego = steps*(rp.E_step + rp.E_base);
        double Tgo = steps*rp.T_step;

        auto back = astar(belief, g, home, 0.65);
        if(back.empty()) continue;
        int bsteps = max(0, (int)back.size()-1);
        double Eback = bsteps*(rp.E_step + rp.E_base);
        double Tback = bsteps*rp.T_step;

        if(budget.E - (Ego+Eback) < rp.E_reserve) continue;
        if(budget.T - (Tgo+Tback) < 0) continue;

        double ig = expectedIG(belief, g, sp);
        double U = ig - lam*steps;
        if(U > bestU){
            bestU=U; bestGoal=g; bestPath=path; found=true;
        }
    }
    return found;
}

int main(){
    int W=35, H=25;
    Grid belief(W,H), truth(W,H);

    // truth: mostly free
    for(int y=0;y<H;y++) for(int x=0;x<W;x++) truth.setv(x,y,0.05);

    // borders
    for(int x=0;x<W;x++){ truth.setv(x,0,0.95); truth.setv(x,H-1,0.95); }
    for(int y=0;y<H;y++){ truth.setv(0,y,0.95); truth.setv(W-1,y,0.95); }

    // random rectangles
    std::mt19937 rng(3);
    auto rint=[&](int a,int b){ std::uniform_int_distribution<int> d(a,b); return d(rng); };
    for(int k=0;k<8;k++){
        int x0=rint(3,W-10), y0=rint(3,H-8);
        int ww=rint(3,7), hh=rint(2,5);
        for(int y=y0;y<min(H-1,y0+hh);y++)
            for(int x=x0;x<min(W-1,x0+ww);x++)
                truth.setv(x,y,0.95);
    }

    Cell home={2,2}, cur=home;

    RobotParams rp;
    SensorParams sp;
    Budget bud{160.0, 140.0};

    applyObs(belief, truth, cur, sp);

    int stepsDone=0, goals=0;

    while(true){
        auto fr = detectFrontiers(belief);
        if(fr.empty()){
            cout << "No frontiers left.\n"; break;
        }
        Cell goal; vector<Cell> path;
        bool ok = pickGoalBudgeted(belief, fr, cur, home, bud, rp, sp, 0.35, goal, path);
        if(!ok){
            cout << "No feasible frontier under budget. Returning.\n";
            break;
        }
        for(size_t i=1;i<path.size();i++){
            bud.E -= (rp.E_step + rp.E_base);
            bud.T -= rp.T_step;
            stepsDone++;
            cur = path[i];
            applyObs(belief, truth, cur, sp);
            if(bud.E < rp.E_reserve || bud.T <= 0) break;
        }
        goals++;
        if(bud.E < rp.E_reserve || bud.T <= 0) break;
        if(goals >= 40) { cout << "Stop: demo goal limit.\n"; break; }
    }

    auto back = astar(belief, cur, home, 0.65);
    if(!back.empty()){
        for(size_t i=1;i<back.size();i++){
            bud.E -= (rp.E_step + rp.E_base);
            bud.T -= rp.T_step;
            cur = back[i];
        }
    }
    cout << "Final pose: ("<<cur.first<<","<<cur.second<<") steps="<<stepsDone<<" goals="<<goals<<"\n";
    cout << "Remaining budget: E="<<bud.E<<" T="<<bud.T<<"\n";
    return 0;
}
      

9. Java Implementation (Standalone)

Java is less common on embedded AMRs today, but the algorithmic structure is identical. If integrating with robotics middleware, typical options include bridging to ROS via network APIs or using Java-based robotics frameworks. The code below demonstrates the full budget-aware loop without middleware.

Code: Chapter17_Lesson4.java


// Chapter17_Lesson4.java
// Exploration Under Limited Battery/Time (Budget-Aware Frontier Exploration)
// Self-contained demo without ROS. Designed for clarity, not maximum performance.
// Compile: javac Chapter17_Lesson4.java
// Run:     java Chapter17_Lesson4
import java.util.*;

public class Chapter17_Lesson4 {

    static class Budget { double E, T; Budget(double E,double T){this.E=E;this.T=T;} }
    static class RobotParams { double EStep=1.2, TStep=1.0, EBase=0.10, EReserve=15.0; }
    static class SensorParams { int r=3; }

    static double clamp(double x,double lo,double hi){ return Math.max(lo, Math.min(hi, x)); }
    static double entropyBern(double p){
        p = clamp(p, 1e-9, 1.0-1e-9);
        return -(p*Math.log(p) + (1.0-p)*Math.log(1.0-p));
    }

    static class Grid {
        int W,H;
        double[] p; // row-major
        Grid(int W,int H){ this.W=W; this.H=H; p=new double[W*H]; Arrays.fill(p, 0.5); }
        boolean inb(int x,int y){ return 0<=x && x<W && 0<=y && y<H; }
        int idx(int x,int y){ return y*W+x; }
        double get(int x,int y){ return p[idx(x,y)]; }
        void set(int x,int y,double v){ p[idx(x,y)] = clamp(v, 0.0, 1.0); }
        boolean isFree(int x,int y,double thr){ return get(x,y) < thr; }
        boolean isOcc(int x,int y,double thr){ return get(x,y) > thr; }
    }

    static class Cell {
        int x,y;
        Cell(int x,int y){ this.x=x; this.y=y; }
        @Override public boolean equals(Object o){
            if(!(o instanceof Cell)) return false;
            Cell c=(Cell)o; return x==c.x && y==c.y;
        }
        @Override public int hashCode(){ return Objects.hash(x,y); }
    }

    static List<Cell> neigh4(Grid g, Cell c){
        int x=c.x,y=c.y;
        int[][] cand={ {x+1,y},{x-1,y},{x,y+1},{x,y-1} };
        ArrayList<Cell> out=new ArrayList<>();
        for(int[] q:cand) if(g.inb(q[0],q[1])) out.add(new Cell(q[0],q[1]));
        return out;
    }

    static List<Cell> astar(Grid g, Cell s, Cell goal, double occThr){
        if(!g.inb(s.x,s.y) || !g.inb(goal.x,goal.y)) return Collections.emptyList();
        if(!g.isFree(s.x,s.y,occThr) || !g.isFree(goal.x,goal.y,occThr)) return Collections.emptyList();

        class Node { double f; Cell c; Node(double f,Cell c){this.f=f;this.c=c;} }
        PriorityQueue<Node> pq=new PriorityQueue<>(Comparator.comparingDouble(a->a.f));
        Map<Cell, Double> gscore=new HashMap<>();
        Map<Cell, Cell> parent=new HashMap<>();

        java.util.function.ToDoubleFunction<Cell> h = (Cell c) -> Math.abs(c.x-goal.x)+Math.abs(c.y-goal.y);

        pq.add(new Node(h.applyAsDouble(s), s));
        gscore.put(s, 0.0);

        while(!pq.isEmpty()){
            Cell cur=pq.poll().c;
            if(cur.equals(goal)){
                ArrayList<Cell> path=new ArrayList<>();
                path.add(cur);
                while(parent.containsKey(cur)){
                    cur = parent.get(cur);
                    path.add(cur);
                }
                Collections.reverse(path);
                return path;
            }
            double gc = gscore.get(cur);
            for(Cell nb: neigh4(g, cur)){
                if(!g.isFree(nb.x, nb.y, occThr)) continue;
                double tent = gc + 1.0;
                if(!gscore.containsKey(nb) || tent < gscore.get(nb)){
                    gscore.put(nb, tent);
                    parent.put(nb, cur);
                    pq.add(new Node(tent + h.applyAsDouble(nb), nb));
                }
            }
        }
        return Collections.emptyList();
    }

    static List<Cell> detectFrontiers(Grid g, double freeThr){
        ArrayList<Cell> fr=new ArrayList<>();
        for(int y=0;y<g.H;y++){
            for(int x=0;x<g.W;x++){
                if(g.get(x,y) >= freeThr) continue;
                Cell c=new Cell(x,y);
                for(Cell nb: neigh4(g,c)){
                    double pn = g.get(nb.x, nb.y);
                    if(Math.abs(pn-0.5) < 0.15){ fr.add(c); break; }
                }
            }
        }
        return fr;
    }

    static double expectedIG(Grid belief, Cell pose, SensorParams sp){
        double ig=0.0;
        for(int dy=-sp.r; dy<=sp.r; dy++){
            for(int dx=-sp.r; dx<=sp.r; dx++){
                int x=pose.x+dx, y=pose.y+dy;
                if(!belief.inb(x,y)) continue;
                double p0=belief.get(x,y);
                double H0=entropyBern(p0);
                double Hpost=0.5*entropyBern(0.95) + 0.5*entropyBern(0.05);
                double w=clamp(1.0 - Math.abs(p0-0.5)*2.0, 0.0, 1.0);
                ig += w * Math.max(0.0, H0 - Hpost);
            }
        }
        return ig;
    }

    static void applyObs(Grid belief, Grid truth, Cell pose, SensorParams sp){
        for(int dy=-sp.r; dy<=sp.r; dy++){
            for(int dx=-sp.r; dx<=sp.r; dx++){
                int x=pose.x+dx, y=pose.y+dy;
                if(!belief.inb(x,y)) continue;
                boolean occ = truth.isOcc(x,y,0.5);
                belief.set(x,y, occ ? 0.95 : 0.05);
            }
        }
    }

    static class Choice { Cell goal; List<Cell> path; double U; Choice(Cell g,List<Cell> p,double U){this.goal=g;this.path=p;this.U=U;} }

    static Choice pickGoalBudgeted(
        Grid belief, List<Cell> frontiers, Cell cur, Cell home,
        Budget bud, RobotParams rp, SensorParams sp, double lam, double occThr
    ){
        if(frontiers.isEmpty()) return null;

        // subsample
        if(frontiers.size() > 80){
            Collections.shuffle(frontiers, new Random(7));
            frontiers = frontiers.subList(0, 80);
        }

        double bestU=-1e18;
        Choice best=null;

        for(Cell g: frontiers){
            List<Cell> path = astar(belief, cur, g, occThr);
            if(path.isEmpty()) continue;
            int steps = Math.max(0, path.size()-1);
            double Ego = steps*(rp.EStep + rp.EBase);
            double Tgo = steps*rp.TStep;

            List<Cell> back = astar(belief, g, home, occThr);
            if(back.isEmpty()) continue;
            int bsteps = Math.max(0, back.size()-1);
            double Eback = bsteps*(rp.EStep + rp.EBase);
            double Tback = bsteps*rp.TStep;

            if(bud.E - (Ego+Eback) < rp.EReserve) continue;
            if(bud.T - (Tgo+Tback) < 0) continue;

            double ig = expectedIG(belief, g, sp);
            double U = ig - lam*steps;
            if(U > bestU){
                bestU=U;
                best = new Choice(g, path, U);
            }
        }
        return best;
    }

    public static void main(String[] args){
        int W=35, H=25;
        Grid belief=new Grid(W,H);
        Grid truth=new Grid(W,H);

        // truth: mostly free
        for(int y=0;y<H;y++) for(int x=0;x<W;x++) truth.set(x,y,0.05);

        // borders
        for(int x=0;x<W;x++){ truth.set(x,0,0.95); truth.set(x,H-1,0.95); }
        for(int y=0;y<H;y++){ truth.set(0,y,0.95); truth.set(W-1,y,0.95); }

        // random rectangles
        Random rng=new Random(3);
        for(int k=0;k<8;k++){
            int x0=3 + rng.nextInt(W-10-3+1);
            int y0=3 + rng.nextInt(H-8-3+1);
            int ww=3 + rng.nextInt(7-3+1);
            int hh=2 + rng.nextInt(5-2+1);
            for(int y=y0;y<Math.min(H-1, y0+hh); y++)
                for(int x=x0;x<Math.min(W-1, x0+ww); x++)
                    truth.set(x,y,0.95);
        }

        Cell home=new Cell(2,2);
        Cell cur=home;

        RobotParams rp=new RobotParams();
        SensorParams sp=new SensorParams();
        Budget bud=new Budget(160.0, 140.0);

        applyObs(belief, truth, cur, sp);

        int stepsDone=0, goals=0;

        while(true){
            List<Cell> fr = detectFrontiers(belief, 0.35);
            if(fr.isEmpty()){
                System.out.println("No frontiers left.");
                break;
            }
            Choice ch = pickGoalBudgeted(belief, fr, cur, home, bud, rp, sp, 0.35, 0.65);
            if(ch == null){
                System.out.println("No feasible frontier under budget. Returning.");
                break;
            }
            for(int i=1;i<ch.path.size();i++){
                bud.E -= (rp.EStep + rp.EBase);
                bud.T -= rp.TStep;
                stepsDone++;
                cur = ch.path.get(i);
                applyObs(belief, truth, cur, sp);
                if(bud.E < rp.EReserve || bud.T <= 0) break;
            }
            goals++;
            if(bud.E < rp.EReserve || bud.T <= 0) break;
            if(goals >= 40){ System.out.println("Stop: demo goal limit."); break; }
        }

        List<Cell> back = astar(belief, cur, home, 0.65);
        if(!back.isEmpty()){
            for(int i=1;i<back.size();i++){
                bud.E -= (rp.EStep + rp.EBase);
                bud.T -= rp.TStep;
                cur = back.get(i);
            }
        }

        System.out.printf("Final pose: (%d,%d) steps=%d goals=%d%n", cur.x, cur.y, stepsDone, goals);
        System.out.printf("Remaining budget: E=%.2f T=%.2f%n", bud.E, bud.T);
    }
}
      

10. MATLAB / Simulink Implementation

MATLAB is excellent for quickly validating exploration logic and budget controllers. For Simulink: implement the selection policy in a MATLAB Function block and manage Explore/Return logic using Stateflow. If you have Robotics System Toolbox, you can also connect to ROS/ROS2 and replace the internal map with live occupancy grids.

Code: Chapter17_Lesson4.m


% Chapter17_Lesson4.m
% Exploration Under Limited Battery/Time (Budget-Aware Frontier Exploration)
% Self-contained MATLAB demo (no toolboxes required).
%
% Notes for Simulink:
% - You can implement the "budget update" and "goal selection" blocks in Simulink
%   using MATLAB Function blocks, then connect a discrete-time integrator for
%   time and energy and a state machine (Stateflow) for Explore/Return states.

clear; clc; rng(7);

W = 35; H = 25;
belief = 0.5 * ones(H, W);        % p_occ belief
truth  = 0.05 * ones(H, W);       % synthetic truth (mostly free)

% Borders as obstacles
truth(1,:) = 0.95; truth(end,:) = 0.95;
truth(:,1) = 0.95; truth(:,end) = 0.95;

% Random rectangles
rng(3);
for k=1:8
    x0 = randi([4, W-10]); y0 = randi([4, H-8]);
    ww = randi([3, 7]);    hh = randi([2, 5]);
    x1 = min(W-1, x0+ww);  y1 = min(H-1, y0+hh);
    truth(y0:y1, x0:x1) = 0.95;
end

home = [3, 3];   % [x,y] with 1-based indexing
cur  = home;

% Robot/budget params
E_step = 1.2; E_base = 0.10; T_step = 1.0;
E_reserve = 15.0;
E = 160.0; T = 140.0;

% Sensor params
r = 3;
p_occ_obs = 0.95; p_free_obs = 0.05;

% Initial observation
belief = apply_observation(belief, truth, cur, r, p_occ_obs, p_free_obs);

goals = 0; stepsDone = 0;

while true
    frontiers = detect_frontiers(belief, 0.35);
    if isempty(frontiers)
        disp("No frontiers left."); break;
    end

    [goal, path] = pick_goal_budgeted(belief, frontiers, cur, home, ...
        E, T, E_step, E_base, T_step, E_reserve, r, 0.35);

    if isempty(goal)
        disp("No feasible frontier under budget. Returning."); break;
    end

    % Execute path (skip first cell)
    for i = 2:size(path,1)
        E = E - (E_step + E_base);
        T = T - T_step;
        stepsDone = stepsDone + 1;
        cur = path(i,:);
        belief = apply_observation(belief, truth, cur, r, p_occ_obs, p_free_obs);
        if (E < E_reserve) || (T <= 0)
            disp("Budget exhausted mid-path."); break;
        end
    end

    goals = goals + 1;
    if (E < E_reserve) || (T <= 0), break; end
    if goals >= 40, disp("Stop: demo goal limit."); break; end
end

fprintf("Final pose: (%d,%d) steps=%d goals=%d\n", cur(1), cur(2), stepsDone, goals);
fprintf("Remaining budget: E=%.2f T=%.2f\n", E, T);

% --------------------------- Helper functions ---------------------------

function frontiers = detect_frontiers(belief, free_thr)
    [H,W] = size(belief);
    frontiers = [];
    for y=1:H
        for x=1:W
            if belief(y,x) >= free_thr, continue; end
            nbs = neighbors4([x,y], W, H);
            for k=1:size(nbs,1)
                xn=nbs(k,1); yn=nbs(k,2);
                if abs(belief(yn,xn) - 0.5) < 0.15
                    frontiers = [frontiers; x,y]; %#ok<AGROW>
                    break;
                end
            end
        end
    end
end

function nbs = neighbors4(c, W, H)
    x=c(1); y=c(2);
    cand = [x+1,y; x-1,y; x,y+1; x,y-1];
    keep = cand(:,1)>=1 & cand(:,1)<=W & cand(:,2)>=1 & cand(:,2)<=H;
    nbs = cand(keep,:);
end

function belief = apply_observation(belief, truth, pose, r, p_occ_obs, p_free_obs)
    [H,W] = size(belief);
    x0=pose(1); y0=pose(2);
    for dy=-r:r
        for dx=-r:r
            x=x0+dx; y=y0+dy;
            if x<1 || x>W || y<1 || y>H, continue; end
            isOcc = truth(y,x) > 0.5;
            belief(y,x) = (isOcc)*p_occ_obs + (~isOcc)*p_free_obs;
        end
    end
end

function ig = expected_info_gain(belief, pose, r)
    % nats, approximate entropy reduction proxy
    x0=pose(1); y0=pose(2);
    [H,W] = size(belief);
    ig = 0.0;
    Hpost = 0.5*bern_entropy(0.95) + 0.5*bern_entropy(0.05);
    for dy=-r:r
        for dx=-r:r
            x=x0+dx; y=y0+dy;
            if x<1 || x>W || y<1 || y>H, continue; end
            p0 = belief(y,x);
            H0 = bern_entropy(p0);
            w = max(0.0, min(1.0, 1.0 - abs(p0-0.5)*2.0));
            ig = ig + w * max(0.0, H0 - Hpost);
        end
    end
end

function H = bern_entropy(p)
    p = max(1e-9, min(1-1e-9, p));
    H = -(p*log(p) + (1-p)*log(1-p));
end

function [goal, bestPath] = pick_goal_budgeted(belief, frontiers, cur, home, ...
        E, T, E_step, E_base, T_step, E_reserve, r, lam)

    goal = []; bestPath = [];
    bestU = -1e18;

    % subsample
    if size(frontiers,1) > 80
        idx = randperm(size(frontiers,1), 80);
        frontiers = frontiers(idx,:);
    end

    for k=1:size(frontiers,1)
        g = frontiers(k,:);
        path = astar_grid(belief, cur, g, 0.65);
        if isempty(path), continue; end

        steps = size(path,1) - 1;
        Ego = steps*(E_step + E_base);
        Tgo = steps*T_step;

        back = astar_grid(belief, g, home, 0.65);
        if isempty(back), continue; end
        bsteps = size(back,1) - 1;
        Eback = bsteps*(E_step + E_base);
        Tback = bsteps*T_step;

        if (E - (Ego+Eback) < E_reserve), continue; end
        if (T - (Tgo+Tback) < 0), continue; end

        ig = expected_info_gain(belief, g, r);
        U = ig - lam*steps;

        if U > bestU
            bestU = U;
            goal = g;
            bestPath = path;
        end
    end
end

function path = astar_grid(belief, s, g, occThr)
    % 4-neighbor A*; blocks if belief(y,x) > occThr
    [H,W] = size(belief);
    if any(s<1) || s(1)>W || s(2)>H || any(g<1) || g(1)>W || g(2)>H
        path = []; return;
    end
    if belief(s(2),s(1)) > occThr || belief(g(2),g(1)) > occThr
        path = []; return;
    end

    % maps (use linear indices)
    idx = @(x,y) (y-1)*W + x;
    pos = @(k) [mod(k-1,W)+1, floor((k-1)/W)+1];

    open = java.util.PriorityQueue();
    gscore = containers.Map('KeyType','int32','ValueType','double');
    parent = containers.Map('KeyType','int32','ValueType','int32');

    hs = abs(s(1)-g(1)) + abs(s(2)-g(2));
    open.add({hs, idx(s(1),s(2))});
    gscore(idx(s(1),s(2))) = 0.0;

    visited = containers.Map('KeyType','int32','ValueType','logical');

    while ~open.isEmpty()
        node = open.remove();
        curk = node{2};
        if isKey(visited, curk), continue; end
        visited(curk) = true;

        c = pos(curk);
        if all(c==g)
            % reconstruct
            path = c;
            while isKey(parent, curk)
                curk = parent(curk);
                path = [pos(curk); path]; %#ok<AGROW>
            end
            return;
        end

        nbs = neighbors4(c, W, H);
        for i=1:size(nbs,1)
            nb = nbs(i,:);
            if belief(nb(2),nb(1)) > occThr, continue; end
            nbk = idx(nb(1),nb(2));
            tent = gscore(curk) + 1.0;
            if ~isKey(gscore, nbk) || tent < gscore(nbk)
                gscore(nbk) = tent;
                parent(nbk) = curk;
                f = tent + abs(nb(1)-g(1)) + abs(nb(2)-g(2));
                open.add({f, nbk});
            end
        end
    end
    path = [];
end
      

11. Wolfram Mathematica (Lagrangian and Greedy Knapsack Demonstrations)

Mathematica is useful for symbolic reasoning about the Lagrangian relaxation and for experimenting with submodular selection rules. The following notebook-style Wolfram Language code (saved as .nb) includes (i) a toy Lagrangian maximization and (ii) a greedy knapsack selection for coverage.

Code: Chapter17_Lesson4.nb


(* Chapter17_Lesson4.nb
   Exploration Under Limited Battery/Time (Budget-Aware Exploration)
   Wolfram Language code (can be pasted into a Mathematica notebook).
*)

ClearAll["Global`*"];

(* 1) Utility / constrained optimization view *)
(* Maximize expected info gain G(x) subject to energy/time cost constraints. *)
(* Use a Lagrangian relaxation: maximize  G(x) - lambda_E C_E(x) - lambda_T C_T(x). *)

G[x_] := Exp[-0.25 (x - 6)^2] + 0.5 Exp[-0.2 (x - 14)^2];   (* toy "information" peaks *)
CE[x_] := 1.2 x + 0.10 x;                                  (* linearized energy cost *)
CT[x_] := x;                                               (* time cost *)

lambdaE = 0.05;
lambdaT = 0.02;

U[x_] := G[x] - lambdaE CE[x] - lambdaT CT[x];

sol = NMaximize[{U[x], 0 <= x <= 20}, x]
Plot[{G[x], U[x]}, {x, 0, 20}, PlotLegends -> {"G(x)", "U(x)"}]

(* 2) Submodular "coverage" objective under knapsack: f(S)=|Union coverage| *)
(* Greedy by marginal gain per cost. *)

elements = Range[0, 19];
items = {
   <|"name" -> "A", "cov" -> {0, 1, 2, 3, 4, 5}, "cost" -> 3.0|>,
   <|"name" -> "B", "cov" -> {4, 5, 6, 7, 8}, "cost" -> 2.0|>,
   <|"name" -> "C", "cov" -> {9, 10, 11, 12}, "cost" -> 2.5|>,
   <|"name" -> "D", "cov" -> {12, 13, 14, 15, 16}, "cost" -> 3.0|>,
   <|"name" -> "E", "cov" -> {16, 17, 18, 19}, "cost" -> 1.8|>,
   <|"name" -> "F", "cov" -> {2, 3, 8, 9, 17}, "cost" -> 2.2|>
};

f[S_] := Length[Union @@ (Lookup[S, "cov"])];

GreedyKnapsack[items_, B_] := Module[{S = {}, covered = {}, rem = items, budget = B, best, bestRatio},
  While[True,
    best = None; bestRatio = -Infinity;
    Do[
      If[item["cost"] <= budget,
        With[{marg = Length[Union[covered, item["cov"]]] - Length[covered]},
          If[marg > 0,
            With[{ratio = marg/item["cost"]},
              If[ratio > bestRatio, bestRatio = ratio; best = item]
            ]
          ]
        ]
      ],
      {item, rem}
    ];
    If[best === None, Break[]];
    AppendTo[S, best];
    covered = Union[covered, best["cov"]];
    budget -= best["cost"];
    rem = DeleteCases[rem, best];
  ];
  <|"chosen" -> Lookup[S, "name"], "coveredCount" -> Length[covered], "budgetLeft" -> budget|>
];

GreedyKnapsack[items, 7.0]
      

12. Problems and Solutions

Problem 1 (Return-feasibility under energy budget): A robot has remaining energy \( E_{\text{rem} }=120 \) (arb. units) and reserve \( E_{\text{res} }=20 \). The estimated energy-per-meter is \( c_E=2 \) and base term is ignored. Current-to-goal distance is \( d(x,g)=15 \) meters and goal-to-home is \( d(g,h)=18 \) meters. Is \( g \) feasible?

Solution: Estimated required travel energy is:

\[ \widehat{C}_E(x\rightarrow g) + \widehat{C}_E(g\rightarrow h) = c_E\big(d(x,g)+d(g,h)\big) = 2(15+18)=66 \]

The feasibility condition is \( E_{\text{rem} } - 66 \ge E_{\text{res} } \), i.e., \( 120-66=54 \ge 20 \). Feasible.

Problem 2 (Lagrangian selection between two frontiers): Two candidate targets have information gains \( \operatorname{IG}_1=9 \), \( \operatorname{IG}_2=7 \). Their travel costs (in steps) are \( c_1=20 \), \( c_2=10 \). Using a utility \( U=\operatorname{IG} - \lambda c \), find the range of \( \lambda \) where target 2 is preferred.

Solution: Prefer 2 when:

\[ 7 - \lambda(10) \ge 9 - \lambda(20) \;\;\Longleftrightarrow\;\; -2 \ge -10\lambda \;\;\Longleftrightarrow\;\; \lambda \ge 0.2 \]

For \( \lambda \ge 0.2 \), cost is weighted strongly enough that the cheaper target wins.

Problem 3 (Submodularity of coverage): Let \( f(S)=\left|\bigcup_{a\in S}\mathcal{V}(a)\right| \). Prove the diminishing returns property: for \( A \subseteq B \) and \( a\notin B \), \( f(A\cup\{a\})-f(A)\ge f(B\cup\{a\})-f(B) \).

Solution: Write the marginal gains as new elements introduced by \( a \):

\[ f(A\cup\{a\})-f(A) = \left|\mathcal{V}(a)\setminus \bigcup_{u\in A}\mathcal{V}(u)\right| \]

\[ f(B\cup\{a\})-f(B) = \left|\mathcal{V}(a)\setminus \bigcup_{u\in B}\mathcal{V}(u)\right| \]

Since \( A \subseteq B \), we have \( \bigcup_{u\in A}\mathcal{V}(u) \subseteq \bigcup_{u\in B}\mathcal{V}(u) \), therefore the set subtracted in the first expression is smaller, and the remaining set is larger: the cardinality cannot decrease when subtracting a smaller set. Hence the inequality holds.

Problem 4 (Greedy gap decay inequality): Assume a monotone submodular \( f \) and a knapsack budget \( B \). Show that if greedy picks \( a_{k+1} \) with cost \( c_{k+1} \), then a typical gap inequality can be written as: \( f(S_{k+1}) - f(S_k) \ge \frac{c_{k+1} }{B}(f(S^\star)-f(S_k)) \) under the “average remaining density” argument.

Solution: Consider an optimal set \( S^\star \) with total cost at most \( B \). By submodularity and monotonicity, the total gain of adding all items in \( S^\star \) to \( S_k \) is:

\[ f(S_k \cup S^\star) - f(S_k) \le \sum_{a\in S^\star}\big(f(S_k\cup\{a\}) - f(S_k)\big) \]

The left-hand side is at least \( f(S^\star)-f(S_k) \) by monotonicity. Hence:

\[ f(S^\star)-f(S_k) \le \sum_{a\in S^\star}\Delta_k(a) \]

where \( \Delta_k(a)=f(S_k\cup\{a\})-f(S_k) \). Divide and multiply by costs and use that \( \sum_{a\in S^\star}c(a)\le B \). There must exist some item in \( S^\star \) whose density satisfies:

\[ \max_{a}\frac{\Delta_k(a)}{c(a)} \ge \frac{f(S^\star)-f(S_k)}{B} \]

Greedy chooses an item with density at least this maximum; thus for the chosen item \( a_{k+1} \): \( \Delta_k(a_{k+1}) \ge \frac{c_{k+1} }{B}(f(S^\star)-f(S_k)) \), proving the stated inequality.

Problem 5 (Triangle-inequality “go/no-go” test on grids): Suppose your planner uses a metric distance \( d \) (shortest path) satisfying triangle inequality. Show that if \( d(x,g) + d(g,h) \le D_{\max} \), then any intermediate point \( y \) on a shortest path from \( x \) to \( g \) also satisfies \( d(y,h) \le D_{\max} - d(x,y) \).

Solution: Let \( y \) lie on a shortest path from \( x \) to \( g \), so \( d(x,g)=d(x,y)+d(y,g) \). Then:

\[ d(y,h) \le d(y,g)+d(g,h) = \big(d(x,g)-d(x,y)\big)+d(g,h) \]

Using the assumption \( d(x,g)+d(g,h)\le D_{\max} \), we get:

\[ d(y,h) \le D_{\max} - d(x,y) \]

This inequality justifies a conservative “keep return margin” rule while executing a path.

13. Summary

We modeled exploration under energy and time budgets, introduced a constrained information-gain objective, and derived practical selection rules via Lagrangian relaxation. We then connected exploration viewpoint selection to submodular maximization under knapsack, explaining why greedy “gain-per-cost” heuristics are effective. Finally, we enforced terminal feasibility (must-return constraint) and implemented a receding-horizon budget-aware frontier explorer in Python, C++, Java, MATLAB, and Mathematica.

14. References

  1. Yamauchi, B. (1997). A frontier-based approach for autonomous exploration. Proceedings of IEEE International Symposium on Computational Intelligence in Robotics and Automation (CIRA).
  2. Burgard, W., Moors, M., Stachniss, C., & Schneider, F.E. (2005). Coordinated multi-robot exploration. IEEE Transactions on Robotics, 21(3), 376–386.
  3. Stachniss, C., Grisetti, G., & Burgard, W. (2005). Information gain-based exploration using Rao–Blackwellized particle filters. Robotics: Science and Systems (RSS).
  4. Singh, A., Krause, A., Guestrin, C., Kaiser, W.J., & Batalin, M.A. (2009). Efficient informative sensing using multiple robots. Journal of Artificial Intelligence Research, 34, 707–755.
  5. Nemhauser, G.L., Wolsey, L.A., & Fisher, M.L. (1978). An analysis of approximations for maximizing submodular set functions. Mathematical Programming, 14, 265–294.
  6. Krause, A., & Guestrin, C. (2007). Near-optimal observation selection using submodular functions. Proceedings of the AAAI Conference on Artificial Intelligence.
  7. Bry, A., & Roy, N. (2011). Rapidly-exploring random belief trees for motion planning under uncertainty. IEEE International Conference on Robotics and Automation (ICRA).
  8. Hollinger, G.A., & Sukhatme, G.S. (2014). Sampling-based robotic information gathering algorithms. The International Journal of Robotics Research, 33(9), 1271–1287.
  9. Charrow, B., Liu, S., Kumar, V., & Michael, N. (2015). Information-theoretic mapping using Cauchy-Schwarz quadratic mutual information. IEEE International Conference on Robotics and Automation (ICRA).
  10. Atanasov, N., Zhu, M., Daniilidis, K., & Pappas, G.J. (2014). Information acquisition with sensing robots: Algorithms and error bounds. IEEE International Conference on Robotics and Automation (ICRA).