Decision Making I: MDPs and Value Iteration
A plan is a line through space; a policy is an answer for every state. This chapter derives the Bellman equation, proves value iteration converges, and shows why noise makes the optimal route longer.
One way to cope with the resulting uncertainty is to generate a policy for action selection defined for all states that the robot might encounter.
In this chapter
Chapter 20 ended with a beautiful path and a robot sliding off it. That is not a bug in the planner. A path is a function of time — do this, then this, then this — and the moment the wheels deliver something other than what was commanded, the robot is at a state the path has nothing to say about. Every chapter since Chapter 9 has been about how reliably the wheels fail to deliver.
This chapter makes the move the rest of Part VI pivots on: stop computing a path and start computing a policy, a function of state that answers "what should I do?" everywhere, so that drifting off is not a failure mode but a Tuesday. The framework is the Markov decision process, the algorithm is value iteration, and the object it produces — the value function — is Chapter 20's potential field done properly: a scalar field that cannot have spurious local minima, because it is defined as expected cost-to-go rather than sculpted out of geometry.
The chapter closes on the question Chapter 22 exists to answer. A policy is a function of the state. What if the robot does not know the state?
The plan that slides off the floor
Here is the experiment, run twice on the same world, with the same solver and the same random numbers. On the left, Rusty commits to the optimal action sequence and replays it open-loop. On the right, Rusty consults the same solver's answer at every step.
At zero slip the two pictures are identical, and that identity is the whole reason deterministic planning ever worked: when execution is perfect, the sequence of actions along the optimal path is the optimal policy restricted to that path. Turn the slip up and the pictures diverge immediately, and they diverge in a specific, diagnostic way.
The plan does not fail gradually. It fails at the first divergence and then accumulates. Once the robot is one cell off the planned trajectory, the remaining actions in the sequence were computed for somebody else's position. Step seventeen of a plan is a claim about where you will be at step seventeen, and after a few unlucky draws it is a claim about a place you have never been.
The policy is unbothered. It never notices the "error", because the notion of error requires a reference trajectory and there isn't one. It reads the current cell and answers. The realized path is longer than the nominal one — noise costs something, and the cost is real — but the arrival rate barely moves. Run that world four thousand times with matched seeds and the split is brutal:
| slip | |||||
|---|---|---|---|---|---|
| open-loop plan reaches the dock | |||||
| policy reaches the dock | |||||
| policy's mean steps (nominal 16) |
A five percent chance of veering — which the next section prices at about of heading uncertainty, an ordinary afternoon for a localizer — already sinks the plan four times out of five. The policy pays for the same noise in steps rather than in failures, and steps are a currency you can budget.
The two objects are different types. This is worth being pedantic about, because the type is the lesson:
A plan is indexed by time. A policy is indexed by state. Everything else in this chapter follows from taking that difference seriously.
The obvious objection is that replanning fixes this: run the planner again from wherever you actually are. It does, and it is what most deployed systems do. But notice what replanning converges to as the replanning rate goes up — an answer for every state you might visit, computed on demand. A policy is what you get when you precompute all of them, and it is worth building once so you can see what the on-demand version is approximating. (When the state space is too big to precompute, you go back to on-demand: that is Chapter 23.)
Building intuition
Paint a reward, get a behavior
The following widget is the chapter in one object. A gridworld is laid over the Apartment floorplan; you paint payoff onto cells; value iteration re-converges live and the arrow field is read off whatever the value function currently is. Nothing is precomputed, and Rusty walks the current policy — including while it is still wrong.
Three things repay a minute of watching, and each one is a theorem later in the chapter.
Value propagates backward from the payoff, roughly a ring per sweep. Immediately after you paint a goal, only its neighbours know about it. That is why value iteration feels like a wave, and why the sweep count scales with the diameter of the map rather than its area. Look closely and the wave is lopsided — it runs further in one direction per sweep than the other — because the backups are done in place, so a cell late in the sweep already sees the cells updated before it. Flip the synchronous sweeps toggle and the lopsidedness disappears, along with some of the speed.
The arrows straighten out long before the numbers do. Watch the residual readout: the arrow
field stops changing while is still large. That is not a rendering
artifact, and it is not luck either —
the loss bound explains why a value
function nowhere near converged still yields a usable policy, and the measurement below shows this
scene's policy going exactly optimal after 45 of its 72 sweeps. argmax only cares about the
ordering of the -values, and orderings settle first.
Slip bends the field away from the hazard. At the arrows run right past the spill cell — why not, you never veer. Push toward and a visible berth opens up. Nobody programmed a safety margin; the margin is what the expectation computes when some of the futures in it involve veering into the thing you are hugging. Measured on the default scene, the optimal policy's mean journey grows from steps at to at to at . The detour margin is the noise, priced.
Everything in that widget is the real algorithm. The sweeps are sweepInPlace from
lib/decision/mdp.ts, the arrows are greedyPolicy, and the robot's steps are drawn from the same
sparse transition rows the solver plans against — the TypeScript twin of the Rust in
Implementation in Rust. If the two ever disagreed, the numerical
self-checks would say so.
The mathematics
Notation
| Symbol | Meaning |
|---|---|
| Finite state and action sets. Here: free grid cells, and the eight compass moves. | |
| Transition model — the same object the Bayes filter predicts with, now indexed by a decision. | |
| Expected immediate payoff of taking u in x. Negative for cost. | |
| Discount factor. γ < 1 for discounted problems; γ = 1 only for stochastic shortest paths. | |
| A deterministic stationary policy — an action for every state, not a sequence. | |
| Expected cumulative discounted payoff over horizon T. | |
| Value of following π from x; value of acting optimally from x. | |
| Value of doing u once, then acting optimally. Chapter 22 reuses these directly. | |
| The Bellman backup operator, (TV)(x) = max_u [ r + γ Σ p V ]. | |
| Slip: probability of veering into each of the two neighbouring directions. Intended direction: 1 − 2s. |
Thrun et al. write the payoff as , collected on entering a state, and put the discount outside the integral. We fold the expected entry payoff into , which is the textbook form and changes not a single number:
The book's grid compiler does exactly this, which is why goal payoff shows up in the neighbours of the goal rather than in the goal cell itself.
The five pieces, and where each one came from
A Markov decision process is the tuple
with three standing assumptions, each of which is load-bearing and each of which is false somewhere.
- Markov. depends on the past only through . This is Chapter 5's assumption, and it is what licenses a policy to be a function of the current state alone. Drop it and the policy needs history.
- Full observability. The robot knows exactly at decision time. This is the assumption this chapter buys its tractability with, and it is the one Chapter 22 pays back.
- Stationarity. and do not depend on . Together with an infinite horizon, this is what makes the optimal policy itself stationary — one lookup table, not one per timestep.
Only the first two pieces are new here. is Chapter 13's occupancy grid with the walls inflated by the robot radius, exactly the configuration-space trick of Chapter 20. is Chapter 9's velocity motion model, discretized. The reward is the only genuinely new modelling decision, and it is where all the honesty lives: an MDP does not tell you what to want, it tells you what to do given what you want.
Where the slip parameter comes from
Gridworld papers announce a slip probability. We are going to earn ours, because the book's promise is that every probability has a pedigree.
The robot commands "move to the neighbouring cell in direction ". It drives for seconds, where is the cell size. The cell it lands in is decided by the bearing of its net displacement: inside the sector around and it landed on the intended neighbour; outside, and it landed on one of the two flanking cells. So
where is drawn by the Chapter 9 sampler: perturb the commanded by and , then integrate the arc for , starting from a heading that is itself uncertain by — because the robot aims using the heading it believes it has.
DerivationIntegrating the velocity model over one cell transit
Start the transit at pose with , the heading error the localizer has left on the table. Thrun et al., Table 5.3 gives the endpoint of an arc with perturbed controls after :
Step 1 — why bearing and not lateral offset. A tempting alternative is to ask whether the lateral displacement exceeds half a cell. It essentially never does: with m you would need . But the grid does not ask "did you drift half a cell sideways", it asks "which cell are you in", and over a single short transit that question is answered by direction. Binning on bearing is the discretization that matches the state space.
Step 2 — the bearing has a closed form, and it is exact. Apply the sum-to-product identities to both components:
The two share a common scalar factor, and that factor is positive whenever — the sign of cancels between and . So
exactly, not to leading order: the chord of a circular arc bisects the turn. And has dropped out entirely — a robot that drives too fast overshoots but does not veer.
Step 3 — collect the variance. With and independent, the bearing is Gaussian, and substituting makes the second term's dependence on speed vanish too:
With and m the turn-rate term contributes rad — a shade under
two degrees — no matter how fast Rusty drives. The library's slipFromVelocityModel samples the
model rather than assuming this, and self-check 11 pins the sampler to the closed form.
Step 4 — read the number. Sampled with seeded draws and full noise, against the formula:
| closed form | ||||
| sampled |
The one place the two rows disagree is instructive. At the sampler reports where the formula says — a factor of forty. The excess is not noise; it is , the fraction of draws in which the Gaussian velocity model of Chapter 9 hands back a negative speed and Rusty reverses into the cell behind him. Below about six degrees of heading uncertainty, the dominant source of "slip" in this model is an artifact of describing a non-negative quantity with a Gaussian. Every noise model has a regime where its tails stop being a harmless approximation, and this is where the velocity model's is.
That table is the most important thing in the section, and it says something the gridworld literature usually hides. A well-localized robot barely slips. At five degrees of heading uncertainty, honest discretized slip is . The slip parameter is not a property of the floor; it is overwhelmingly a property of the localizer. Discretized slip is a state-estimation error wearing a motion-model costume — which is a broad hint that the honest version of this chapter's problem is Chapter 22's.
The widgets let you set up to anyway. Read those settings as "a robot with a bad localizer, a wet floor, or 0.3 m cells that are too coarse for its dynamics" — all three are real, and the middle one is why the parameter is worth a slider.
Return, and why buys convergence
Fix a policy and run it forever. The return is the discounted sum of payoffs,
Discounting does two jobs, and it is worth separating them because only one is mathematical.
The mathematical job: if then , so an infinite sum of infinitely many terms is a finite number and the whole theory has something to be about. Without it, "reach the dock eventually" and "reach the dock in nine steps" both score and has nothing to chew on.
The modelling job: is a statement about how far ahead the robot cares. The effective horizon is steps, in the sense that payoff beyond it is discounted below :
| effective horizon |
For a 0.3 m grid, means the robot can see about 15 metres of consequence. Set and the charging dock becomes invisible from the far bedroom — the value function goes flat out there, the arrows go arbitrary, and the robot wanders. That failure is not a bug in the solver; it is the solver correctly reporting that under your stated preferences, the dock is not worth walking to. You can watch it happen with the Policy Painter's slider.
The Bellman optimality equation
Define the optimal value function and the state–action value
— the value of committing to once and behaving optimally forever after. Then the whole of dynamic programming is the observation that is the pointwise max of :
Read it as a sentence: the best you can do from here is the best over first moves of (what that move pays now) plus (discounted, averaged over where it might land you, the best you can do from there). The colors match the widget below: green is the immediate payoff, blue is the discounted future, purple is the answer.
That widget exists because the equation above looks like linear algebra and is not. A backup is clerical work: read a handful of neighbours, weight them by how likely you are to reach them, scale by , add the payoff, keep the biggest. Step it a few times and watch the value wave crawl backwards from the goal and bend through the doorway — the same wave Chapter 20's wave-front planner produced, which is not a coincidence and is proved below.
DerivationDeriving the Bellman equation from the finite-horizon recursion
Thrun et al. build this by induction on the horizon, and so will we, because the induction is where the optimal-substructure argument actually lives.
Step 1 — horizon one. With exactly one action left, there is no future to trade against:
Step 2 — horizon two, by conditioning on the first move. Take an action , collect , land in with probability , and then you have a one-step problem left. The best you can do from with one step left is by definition, so
Step 3 — why the tail is allowed to be optimal. This is the step that deserves suspicion. The claim is that the tail of an optimal -step plan is an optimal -step plan. It is true here for a specific reason: the payoff decomposes additively across time, the discount factors out, and the future dynamics depend on the past only through (Markov). So the total is , and the tail term is maximized independently of the choice of — for each possible separately. Break any of those three properties and the induction fails; risk-sensitive objectives, for instance, are not additively decomposable and genuinely do not admit this argument.
Step 4 — induct.
Step 5 — take the limit. With , the horizon- and horizon- values differ by at most the tail of a geometric series, , which vanishes for . So is Cauchy in the sup norm and converges to some ; passing to the limit inside the finite max and finite sum gives . The next derivation shows that fixed point is unique, so .
Value iteration converges, and here is the stopping rule
Write the Bellman equation as an operator on value functions:
Value iteration is , starting from . The theorem that makes it an algorithm rather than a hope is that is a contraction.
DerivationT is a γ-contraction in the sup norm, and the stopping bound that follows
Step 1 — the max inequality. For any two families of reals , over a finite index set,
Proof: let . Then . Swap the roles of and for the other direction. (Exercise 1 asks you to exhibit vectors where it is tight.)
Step 2 — apply it to the backup. Fix and set , . The rewards cancel:
Step 3 — push the expectation through. The transition row is a probability distribution, so it is an averaging operator and cannot amplify:
Step 4 — take the sup over . . Since and the space of bounded value functions with the sup norm is complete, Banach's fixed-point theorem gives a unique fixed point and geometric convergence from any start.
Step 5 — the stopping rule. You cannot measure ; you can measure the change one sweep made. Chain them with the triangle inequality:
So to guarantee , stop when a sweep moves the value
function by less than . That is the entire content of
stoppingThreshold(gamma, eps) in the library, and it is why a request for
at actually iterates until sweeps move things by
: at near one, the residual you can see is a wild underestimate of the
error you have.
The factor is the single most common way to be wrong about a value function. At it is : a sweep that changes nothing by more than may still leave you a full unit of reward away from the truth. Reporting "converged" because the residual looked small is how you get a policy that is confidently suboptimal in the one region you cared about.
A half-converged value function still gives a good policy
Greedy extraction is the reason value iteration is useful at all:
DerivationGreedy on V* is optimal; greedy on a nearby V is nearly optimal
Part A — exactness at the fixed point. Let and let be the linear backup that follows without a max: . By the definition of , , so is a fixed point of . But is also a -contraction (Steps 3–4 above never used the max), so its fixed point is unique, and its fixed point is by definition . Hence : greedy is not merely good, it is exact.
Part B — the loss bound. Let and . Three inequalities chained:
For (ii): because is greedy for ; and . For (i): , so . Substituting and solving for :
Two readings of that bound, and they point in opposite directions.
Pessimistic: the amplification factor is at . A value function good to certifies only a policy within of optimal — nearly vacuous.
Optimistic, and what actually happens: the bound is worst-case over adversarial MDPs. In a gridworld, depends only on the order of the -values at each state, and orderings stabilize long before magnitudes do. Measured on the Apartment scene, the greedy policy becomes exactly optimal — identical to in all 925 free cells — after 45 of the 72 sweeps value iteration eventually takes. The residual at that moment is , so the theorem certifies only , on a problem whose optimal value at the start is . The bound is not wrong; it is simply not the thing that happens.
That gap is what real-time dynamic programming (Barto, Bradtke and Singh, 1995) is built to exploit: you may act on a value function you have no right to trust, as long as you keep backing it up along the states you actually visit. It is also why the Policy Painter can run the robot while the wave is still crossing the map.
Policy iteration: solve, improve, repeat
Value iteration nudges every state a little on every sweep. Policy iteration takes the other extreme: pick a policy, evaluate it exactly, then improve it everywhere at once.
The evaluation step is where the linear algebra finally shows up. Fix ; there is no max any more, so the Bellman equation for is a linear system:
with the row-stochastic matrix — sparse, with at most three nonzeros per row in our gridworld. That is the same shape of solve as the sparse Cholesky in Chapter 15, and the Rust implementation hands it to the same crate.
DerivationPolicy iteration improves monotonically and terminates
Step 1 — the improvement is not worse. Let be greedy with respect to . By construction , pointwise.
Step 2 — monotone operators propagate that. is monotone: if pointwise then , because it only adds a fixed vector and averages with nonnegative weights. Applying it repeatedly to gives for every , and the left side converges to . Hence pointwise. (This is the policy improvement theorem, and notice it is the monotonicity, not the contraction, doing the work.)
Step 3 — strictness and termination. If and have the same value function, then , so and is optimal. Otherwise the improvement is strict at some state. There are finitely many deterministic stationary policies — of them — and the sequence of values is strictly increasing, so no policy repeats and the loop terminates.
Step 4 — what happens in practice. The bound is astronomical and irrelevant. On the four-cell hallway below, policy iteration finishes in one improvement. On the 927-state Apartment with it takes 23, the first of which fixes 178 states and the last of which fixes 3. The rule of thumb — iterations grow like of the state count, not like the state count — is observed everywhere and proved nowhere useful.
Step 5 — the interpolation. You do not have to solve the linear system exactly. Doing linear backups instead is modified policy iteration: is value iteration, is Howard's policy iteration, and the sweet spot for large sparse problems is usually .
Stochastic shortest paths: what navigation actually is
Discounting is a strange thing to want from a delivery robot. Nobody prefers a package delivered now over the same package delivered in a minute by a factor of ; we just want it delivered, quickly, and the discount was a mathematical convenience. The formulation that says what we mean is the stochastic shortest path (SSP):
- — no discounting;
- one or more absorbing goal states with zero payoff;
- per step (or ), so every action strictly hurts.
Then is literally the minimum expected number of steps to the goal, which is a quantity you can hold in your head. But the contraction argument is gone: makes the modulus , and there is no Banach theorem to invoke. What replaces it is a condition on the problem rather than on the discount.
Both conditions are checkable, and both fail in ways you will meet. Condition (i) fails if the
inflated map has a walled-off room: the goal is unreachable, there, and a solver
that does not detect it will happily return and an arrow field of noise. Condition (ii)
fails if you give the robot a free action — a zero-cost stay — because then "stand still forever"
is an improper policy with finite cost , and it is optimal. This is not a hypothetical; it is
the single most common bug in hand-rolled SSP solvers, and it is why the library's stay action
still charges stepCost.
The wave-front planner, unmasked
Now set the slip to zero. Each action has exactly one successor, , so the sum over collapses to a single term and the SSP Bellman equation becomes
That is the Bellman–Ford relaxation. Sweep it synchronously and you have Bellman–Ford. Sweep it in-place in order of increasing — always expanding the unfinished state closest to the goal — and you have Dijkstra. Fill it outward from the goal one ring at a time and you have Chapter 20's wave-front planner, exactly.
So the wave-front planner is not analogous to value iteration; it is value iteration, on a deterministic MDP with unit costs, with a particularly clever sweep order. Everything this chapter adds is what happens when the arrow out of a cell is no longer a promise. And the "potential field with no local minima" that Chapter 20 wanted is now definitional: has no spurious local optimum because it is not a field somebody sculpted, it is the expected cost-to-go, and a state whose neighbours are all worse than it would be violating its own Bellman equation.
How much detour is noise worth?
We have said twice now that noise makes the optimal route longer. Here is the quantitative version, in the smallest world that can carry it: two corridors from start to goal, one short and flanked by a drop, one long and flanked by walls.
Before touching the widget, commit to a guess. The ledge is cells; the detour is — nearly three times as long. A veer on the ledge costs a penalty of and dumps the robot back at the start. At what slip does the optimal policy abandon the ledge?
Write your number down, then read the derivation.
DerivationClosed-form route values and the critical slip
Let be the probability that a step goes as commanded, the ledge length, the detour length, and the cliff penalty.
The detour. A veer bumps a wall, costing one step and no progress. The number of attempts needed for one cell of progress is geometric with success probability , so its mean is , and by linearity
Note this is already worse than : even the safe route pays for noise.
The ledge. Let be the value with cells still to go, so , and let be the value at the start. Each step costs , and with probability the robot goes over, pays , and restarts at the start:
Write , a constant with respect to . Then with unrolls to a geometric sum:
Now impose self-consistency at , where . Writing :
using . Substituting and :
Sanity checks. As , and , so : the deterministic answer. And blows up like , exponentially in the length of the exposure, while degrades only like . That asymmetry is the whole story.
The critical slip. is the root of . It has no closed form, so bisect — on the two formulas, not on a simulation. For , , :
Six and a bit percent. Most readers guess somewhere between and , because the detour is so much longer. The values say otherwise:
| slip | ||||||
|---|---|---|---|---|---|---|
| ledge | ||||||
| detour |
The ledge is exponentially fragile and the detour is merely linearly annoying, so they cross early. And notice what corresponds to in the units of the previous section: about of heading uncertainty. The decision of whether to take the short route through the narrow gap is being made, in effect, by the localizer.
Two honest caveats the widget makes visible. First, long after the policy has switched, individual runs on the ledge still sometimes beat the detour — expectation is a claim about the average and nothing else, and if you need a guarantee about the worst case you want a risk-sensitive objective (Akella et al., 2025), which is a different and harder problem. Second, the mean realized return converges to slowly and from either side; twenty finished runs tell you almost nothing.
The algorithms
- In
- transition model, reward, discount, target accuracy
- Out
- V within ε of V*, and the greedy policy π
- for all do
- repeat
- for all do
- endfor
- until
- return
This is Thrun et al.'s Table 15.1 with two additions: the stopping rule from the contraction proof
(line 9), and the explicit policy extraction (line 10). One subtlety hides in line 6. If
is a single array that you write into as you go, later states in the sweep see the updated values
of earlier ones — that is the Gauss–Seidel or asynchronous variant, and it is what the library
does by default. If you write into a fresh array, it is the synchronous Jacobi variant. Thrun's
draft is explicit that this does not affect whether value iteration converges, only how fast. On
the Apartment, Gauss–Seidel needs 72 sweeps where Jacobi needs 84 — and if you order the sweep by
breadth-first distance from the goal, so that every backup reads neighbours that were updated
moments ago, it drops to 33. Same algorithm, same fixed point, less than half the work, purely
from the order of a for loop.
- In
- transition model, reward, discount
- Out
- V*, π* — exactly, in finitely many steps
- initialize arbitrarily
- repeat
- evaluate: solve
- improve:
- if then return
- forever
- In
- the MDP, a target accuracy, and a warm start
- Out
- V, backed up only where it mattered
- empty max-heap; for all with residual : push with key
- while nonempty do
- pop-max; if its key is stale, continue
- ; ;
- for each predecessor of do
- if then push with key
- return
The priority key on line 6 is an upper bound on how much this backup could possibly move that predecessor — so the heap orders states by potential change without paying for a trial backup. Moore and Atkeson (1993) introduced this for learned models; it is just as useful for known ones.
Prioritized sweeping is not universally faster, and it is worth knowing when it loses. Solving the Apartment MDP cold takes 226,050 prioritized backups against 86,400 for 72 Gauss–Seidel sweeps — 2.6× worse, because when every state needs updating, the heap is pure overhead. Its win is repair: after painting one new hazard cell, prioritized sweeping restores optimality in 4,487 backups where warm-started Gauss–Seidel needs 18 sweeps, or 21,600. That is the regime the Policy Painter lives in, and the regime a robot with a changing map lives in.
A worked example you can check by hand
Four states in a line: , , , and the goal , which absorbs. Rusty has two actions.
roll— nudge forward. Advances one cell with probability ; with probability the wheels spin and nothing happens. Costs .lunge— dump enough current into the motors to guarantee the cell change. Costs .
Undiscounted, : a stochastic shortest path. Every number below is reproduced by a Rust
unit test and by self-check 1 in lib/decision/__checks_ch21__.ts.
The fixed point, by hand
Work backwards from the goal. At , roll gives
, and if roll is optimal there, that
equals :
which is just : the expected number of attempts to make one cell of progress. Each cell costs the same, so
Is roll really optimal? Check the alternative at each state: lunge from scores against
; from , against ; from , against . Buying
determinism at a price of is a bad deal when the stochastic option costs in expectation.
The max gate has something to do at every state, and it always makes the same choice.
The first three sweeps
Now run the algorithm synchronously from and watch it get there. Every entry is one line of arithmetic:
| sweep | ||||
|---|---|---|---|---|
Check sweep 3 at yourself: . And notice the shape of the convergence. After three sweeps is 99% of the way to its answer and is only 80%; after four, is done to three decimals and still is not. Information flows backward from the goal at one cell per synchronous sweep, which is the whole reason sweep order matters.
Value iteration needs 24 sweeps to reach here. Policy iteration needs one
improvement: evaluate the initial all-roll policy exactly, discover it is already greedy, stop.
Implementation in Rust
The module is crates/ch21_mdp. The design constraint that shapes everything: a gridworld MDP has
tens of thousands of states and at most three successors per state-action, so the transition model
must be sparse and the inner loop must not allocate.
The model
use nalgebra::DVector;
/// One successor and its probability. `p` is f32 because a transition row is
/// read far more often than it is written, and halving the row halves the cache
/// misses in the inner loop — the only place this crate spends time.
#[derive(Clone, Copy, Debug)]
pub struct Transition {
pub s: u32,
pub p: f32,
}
/// A sparse row of p(· | x, u). Probabilities sum to 1 up to f32 epsilon.
pub type SparseDist = Vec<Transition>;
/// A finite MDP with a compile-time action count.
///
/// `A` is a const generic because the action set of a gridworld is fixed by its
/// connectivity (4, 8, or 9 with `stay`), and pinning it lets the per-state
/// arrays live inline instead of behind a second indirection.
pub struct Mdp<const A: usize> {
pub n_states: usize,
/// Indexed [state][action].
pub trans: Vec<[SparseDist; A]>,
/// r(x, u): the expected immediate payoff, with entry payoff folded in.
pub reward: Vec<[f64; A]>,
/// γ ∈ (0, 1]. γ = 1 is legal only when `absorbing` is non-empty (SSP).
pub gamma: f64,
/// V(x) ≡ 0 here, and the episode stops.
pub absorbing: Vec<bool>,
pub action_labels: [&'static str; A],
}
impl<const A: usize> Mdp<A> {
/// Q(x, u) = r(x, u) + γ Σ_{x'} p(x' | x, u) V(x').
#[inline]
pub fn q(&self, v: &[f64], x: usize, u: usize) -> f64 {
if self.absorbing[x] {
return 0.0;
}
let mut acc = 0.0;
for t in &self.trans[x][u] {
acc += f64::from(t.p) * v[t.s as usize];
}
self.reward[x][u] + self.gamma * acc
}
/// The max gate: (TV)(x) and the action that attains it.
#[inline]
pub fn backup(&self, v: &[f64], x: usize) -> (f64, u8) {
if self.absorbing[x] {
return (0.0, 0);
}
let mut best = f64::NEG_INFINITY;
let mut arg = 0u8;
for u in 0..A {
let q = self.q(v, x, u);
if q > best {
best = q;
arg = u as u8;
}
}
(best, arg)
}
/// ‖TV − V‖∞ over non-absorbing states. Zero exactly at the fixed point,
/// which makes it the only honest convergence certificate.
pub fn max_residual(&self, v: &DVector<f64>) -> f64 {
(0..self.n_states)
.filter(|&x| !self.absorbing[x])
.map(|x| (self.backup(v.as_slice(), x).0 - v[x]).abs())
.fold(0.0, f64::max)
}
}Value iteration, with the stopping rule that the proof earned
use nalgebra::DVector;
use crate::mdp::Mdp;
pub struct ViResult {
pub v: DVector<f64>,
pub policy: Vec<u8>,
pub sweeps: usize,
/// ‖V_k − V_{k−1}‖∞ after each sweep — the widgets' convergence wave.
pub residuals: Vec<f64>,
pub converged: bool,
}
/// Stop when one sweep moves V by less than this, and ‖V − V*‖∞ ≤ ε is
/// guaranteed. From ‖V_k − V*‖ ≤ γ/(1−γ)·‖V_k − V_{k−1}‖.
///
/// γ = 1 (a stochastic shortest path) has no such bound: the contraction
/// modulus is 1 in the sup norm, so we fall back to the raw residual and the
/// caller is on the hook for a proper policy existing.
pub fn stopping_threshold(gamma: f64, eps: f64) -> f64 {
if gamma >= 1.0 { eps } else { eps * (1.0 - gamma) / gamma }
}
/// One in-place Gauss–Seidel sweep. Returns ‖ΔV‖∞.
///
/// In place on purpose: a backup late in the sweep sees the states updated
/// earlier in it, so with a good ordering information crosses the whole map in
/// a single pass instead of one cell per sweep.
pub fn sweep_in_place<const A: usize>(mdp: &Mdp<A>, v: &mut DVector<f64>) -> f64 {
let mut residual: f64 = 0.0;
for x in 0..mdp.n_states {
if mdp.absorbing[x] {
v[x] = 0.0;
continue;
}
let next = mdp.backup(v.as_slice(), x).0;
residual = residual.max((next - v[x]).abs());
v[x] = next;
}
residual
}
/// Thrun et al., Table 15.1 — `MDP_value_iteration`.
pub fn value_iteration<const A: usize>(mdp: &Mdp<A>, eps: f64, max_sweeps: usize) -> ViResult {
let mut v = DVector::zeros(mdp.n_states);
let threshold = stopping_threshold(mdp.gamma, eps);
let mut residuals = Vec::new();
let converged = loop {
if residuals.len() >= max_sweeps {
break false;
}
let r = sweep_in_place(mdp, &mut v);
residuals.push(r);
if r < threshold {
break true;
}
};
ViResult { policy: greedy_policy(mdp, &v), sweeps: residuals.len(), v, residuals, converged }
}
/// π(x) = argmax_u Q(x, u). Optimal once V = V*; within 2γε/(1−γ) before that.
pub fn greedy_policy<const A: usize>(mdp: &Mdp<A>, v: &DVector<f64>) -> Vec<u8> {
(0..mdp.n_states).map(|x| mdp.backup(v.as_slice(), x).1).collect()
}Policy evaluation as a sparse solve
The exact evaluation step is where faer earns its place. The matrix is sparse,
nonsymmetric, and — for — strictly diagonally dominant by rows, which means it is
nonsingular and an LU with partial pivoting is stable without any reordering heroics.
use faer::sparse::{SparseColMat, Triplet};
use faer::prelude::*;
use nalgebra::DVector;
use crate::mdp::Mdp;
use crate::vi::greedy_policy;
/// Solve (I − γ P_π) V^π = r_π exactly.
///
/// Same shape of problem as the normal equations in Chapter 15, and the same
/// crate solves it — the difference is that here the matrix is nonsymmetric, so
/// it is LU rather than Cholesky.
pub fn policy_evaluation<const A: usize>(mdp: &Mdp<A>, pi: &[u8]) -> DVector<f64> {
let n = mdp.n_states;
let mut triplets: Vec<Triplet<usize, usize, f64>> = Vec::with_capacity(4 * n);
let mut rhs = Mat::<f64>::zeros(n, 1);
for x in 0..n {
if mdp.absorbing[x] {
// V(x) ≡ 0: a 1×1 identity row keeps the system square and the
// absorbing convention explicit rather than implied.
triplets.push(Triplet::new(x, x, 1.0));
continue;
}
let u = pi[x] as usize;
triplets.push(Triplet::new(x, x, 1.0));
for t in &mdp.trans[x][u] {
// Accumulating duplicates is exactly right here: a self-loop lands
// on the diagonal and must be subtracted from the identity.
triplets.push(Triplet::new(x, t.s as usize, -mdp.gamma * f64::from(t.p)));
}
rhs[(x, 0)] = mdp.reward[x][u];
}
let a = SparseColMat::try_new_from_triplets(n, n, &triplets)
.expect("policy transition matrix is well formed by construction");
let v = a.sp_lu().expect("I − γP_π is nonsingular for γ < 1").solve(&rhs);
DVector::from_iterator(n, (0..n).map(|i| v[(i, 0)]))
}
/// Howard's policy iteration: evaluate exactly, improve greedily, repeat.
pub fn policy_iteration<const A: usize>(mdp: &Mdp<A>, max_iter: usize) -> (DVector<f64>, Vec<u8>, usize) {
let mut pi = vec![0u8; mdp.n_states];
let mut v = DVector::zeros(mdp.n_states);
for it in 0..max_iter {
v = policy_evaluation(mdp, &pi);
let next = greedy_policy(mdp, &v);
// Switch only on a strict improvement. Ties between equally good
// actions would otherwise make the loop oscillate forever.
let mut changed = 0usize;
let mut merged = pi.clone();
for x in 0..mdp.n_states {
let (a, b) = (next[x] as usize, pi[x] as usize);
if a != b && mdp.q(v.as_slice(), x, a) > mdp.q(v.as_slice(), x, b) + 1e-12 {
merged[x] = next[x];
changed += 1;
}
}
pi = merged;
if changed == 0 {
return (v, pi, it + 1);
}
}
(v, pi, max_iter)
}Compiling a world into an MDP
This is the function that keeps the book's promise. It takes Chapter 13's occupancy grid and Chapter 9's velocity model and produces a finite MDP with no invented constants.
use rand::rngs::SmallRng;
use rand::SeedableRng;
use rand_distr::{Distribution, Normal};
use crate::mdp::{Mdp, SparseDist, Transition};
/// The eight compass moves, counter-clockwise from north.
pub const MOVES8: [(i32, i32); 8] =
[(0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1), (1, 0), (1, 1)];
pub struct GridSpec {
pub width: usize,
pub height: usize,
pub blocked: Vec<bool>,
/// Payoff collected on *entering* a cell. Goals positive, hazards negative.
pub payoff: Vec<f64>,
pub terminal: Vec<bool>,
/// Probability of veering into *each* lateral neighbour. Intended: 1 − 2s.
pub slip: f64,
pub gamma: f64,
pub step_cost: f64,
pub no_corner_cutting: bool,
}
/// The slip parameter, derived rather than decreed: drive the Chapter 9
/// velocity model one cell and bin the displacement by which neighbour its
/// bearing points at.
///
/// `sigma_theta` is the heading error the localizer has left on the table, and
/// it dominates. The closed form derived in the text is
///
/// s = Φ( −(π/8) / sqrt(σ_θ² + α₃ℓ²/4) )
///
/// so with α₃ = 0.05 and ℓ = 0.3 m the wheels contribute a fixed 0.034 rad
/// regardless of speed, while fifteen degrees of heading error puts s at 0.068.
/// We sample rather than evaluate the formula so that changing the motion model
/// changes the answer, which is the whole point of deriving it.
pub fn slip_from_velocity_model(
v: f64, cell: f64, alpha1: f64, alpha3: f64, sigma_theta: f64, seed: u64,
) -> f64 {
let mut rng = SmallRng::seed_from_u64(seed); // never thread_rng: demos are reproducible
let dt = cell / v;
let half_sector = std::f64::consts::FRAC_PI_8;
let heading = Normal::new(0.0, sigma_theta.max(1e-9)).unwrap();
let dv = Normal::new(0.0, (alpha1 * v * v).sqrt().max(1e-12)).unwrap();
let dw = Normal::new(0.0, (alpha3 * v * v).sqrt().max(1e-12)).unwrap();
const N: usize = 40_000;
let lateral = (0..N)
.filter(|_| {
let th0 = heading.sample(&mut rng);
let vh = v + dv.sample(&mut rng);
let wh = dw.sample(&mut rng);
// Thrun et al., Table 5.3: the arc traced by perturbed (v, ω).
let (dx, dy) = if wh.abs() < 1e-9 {
(vh * th0.cos() * dt, vh * th0.sin() * dt)
} else {
let r = vh / wh;
(
-r * th0.sin() + r * (th0 + wh * dt).sin(),
r * th0.cos() - r * (th0 + wh * dt).cos(),
)
};
dy.atan2(dx).abs() > half_sector
})
.count();
// Both sides together are 2s.
(lateral as f64 / N as f64 / 2.0).min(0.49)
}
/// Compile a spec into a finite MDP over the eight compass moves.
///
/// The rule, in one sentence: the commanded direction happens with probability
/// 1 − 2s, each 45° neighbour of it with probability s, and any outcome that
/// would leave the map or enter a wall leaves the robot where it was — while
/// still charging for the attempt, because the wheels turned.
pub fn grid_world_mdp(spec: &GridSpec) -> Mdp<8> {
let n = spec.width * spec.height;
let s = spec.slip.clamp(0.0, 0.49);
let idx = |i: i32, j: i32| (j as usize) * spec.width + (i as usize);
let free = |i: i32, j: i32| {
i >= 0 && j >= 0 && (i as usize) < spec.width && (j as usize) < spec.height
&& !spec.blocked[idx(i, j)]
};
let mut trans = Vec::with_capacity(n);
let mut reward = Vec::with_capacity(n);
let mut absorbing = vec![false; n];
for j in 0..spec.height as i32 {
for i in 0..spec.width as i32 {
let x = idx(i, j);
absorbing[x] = spec.blocked[x] || spec.terminal[x];
let mut rows: [SparseDist; 8] = Default::default();
let mut rs = [0.0f64; 8];
for a in 0..8usize {
let land = |dir: usize| -> u32 {
let (di, dj) = MOVES8[dir % 8];
if !free(i + di, j + dj) { return x as u32; }
if spec.no_corner_cutting && di != 0 && dj != 0
&& (!free(i + di, j) || !free(i, j + dj)) {
return x as u32;
}
idx(i + di, j + dj) as u32
};
let row = condense(&[
Transition { s: land(a), p: (1.0 - 2.0 * s) as f32 },
Transition { s: land(a + 7), p: s as f32 },
Transition { s: land(a + 1), p: s as f32 },
]);
// r(x,u) = −cost(u) + Σ p(x'|x,u)·payoff(x'), so goal payoff is
// collected by the *neighbours* of the goal.
let (di, dj) = MOVES8[a];
let len = ((di * di + dj * dj) as f64).sqrt();
rs[a] = -spec.step_cost * len
+ row.iter().map(|t| f64::from(t.p) * spec.payoff[t.s as usize]).sum::<f64>();
rows[a] = row;
}
trans.push(rows);
reward.push(rs);
}
}
Mdp {
n_states: n, trans, reward, gamma: spec.gamma, absorbing,
action_labels: ["N", "NW", "W", "SW", "S", "SE", "E", "NE"],
}
}
/// Merge duplicate successors and renormalize. Sparse rows must sum to 1, and
/// after wall-clamping they very often do not without this.
fn condense(pairs: &[Transition]) -> SparseDist {
let mut out: SparseDist = Vec::with_capacity(pairs.len());
for t in pairs.iter().filter(|t| t.p > 0.0) {
match out.iter_mut().find(|o| o.s == t.s) {
Some(o) => o.p += t.p,
None => out.push(*t),
}
}
let total: f32 = out.iter().map(|t| t.p).sum();
for t in out.iter_mut() { t.p /= total; }
out.sort_unstable_by_key(|t| t.s);
out
}The worked example, as a test
use ch21_mdp::mdp::{Mdp, Transition};
use ch21_mdp::vi::value_iteration;
/// A, B, C, G in a line. `roll` advances w.p. p and costs 1; `lunge` is
/// deterministic and costs 2. γ = 1, G absorbing: a stochastic shortest path.
pub fn hallway_ssp(p: f64, lunge_cost: f64) -> Mdp<2> {
let mut trans = Vec::new();
let mut reward = Vec::new();
for x in 0..4usize {
if x == 3 {
trans.push([vec![Transition { s: 3, p: 1.0 }], vec![Transition { s: 3, p: 1.0 }]]);
reward.push([0.0, 0.0]);
continue;
}
trans.push([
vec![
Transition { s: x as u32, p: (1.0 - p) as f32 },
Transition { s: x as u32 + 1, p: p as f32 },
],
vec![Transition { s: x as u32 + 1, p: 1.0 }],
]);
reward.push([-1.0, -lunge_cost]);
}
Mdp {
n_states: 4, trans, reward, gamma: 1.0,
absorbing: vec![false, false, false, true],
action_labels: ["roll", "lunge"],
}
}
fn main() {
let mdp = hallway_ssp(0.8, 2.0);
let out = value_iteration(&mdp, 1e-12, 10_000);
println!("V* = {:?}", out.v.as_slice()); // [-3.75, -2.5, -1.25, 0.0]
println!("π* = {:?}", out.policy); // [0, 0, 0, 0] (all `roll`)
println!("sweeps = {}", out.sweeps); // 24
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use nalgebra::DVector;
/// The chapter's hand-computed fixed point, to the digit.
#[test]
fn hallway_ssp_worked_example() {
let mdp = hallway_ssp(0.8, 2.0);
let out = value_iteration(&mdp, 1e-12, 10_000);
for (got, want) in out.v.iter().zip([-3.75, -2.5, -1.25, 0.0]) {
assert_relative_eq!(got, &want, epsilon = 1e-9);
}
// Determinism at 2 per cell is a bad deal against 1/0.8 = 1.25.
assert_eq!(out.policy, vec![0, 0, 0, 0]);
}
/// And the first three sweeps, which the chapter prints as a table.
#[test]
fn hallway_ssp_first_three_sweeps() {
let mdp = hallway_ssp(0.8, 2.0);
let mut v = DVector::zeros(4);
let want = [
[-1.0, -1.0, -1.0, 0.0],
[-2.0, -2.0, -1.2, 0.0],
[-3.0, -2.36, -1.24, 0.0],
];
// Synchronous, to match the table: Gauss–Seidel would already be ahead.
for row in want {
let mut next = v.clone();
for x in 0..4 { next[x] = mdp.backup(v.as_slice(), x).0; }
v = next;
for (got, w) in v.iter().zip(row) {
assert_relative_eq!(got, &w, epsilon = 1e-12);
}
}
}
}Putting it together
Run the whole thing on the Apartment: 0.3 m cells, walls inflated by 0.16 m for Rusty's radius, cells of which 927 are free states, eight compass moves, no corner cutting, , , a charging dock worth and a spill worth .
| method | work to | notes |
|---|---|---|
| value iteration, synchronous (Jacobi) | 84 sweeps | 100,800 backups |
| value iteration, in place (Gauss–Seidel) | 72 sweeps | 86,400 backups — 14% fewer, for free |
| the same, swept in breadth-first order from the goal | 33 sweeps | 39,600 backups — the order is the algorithm |
| policy iteration | 23 improvements | 23 sparse solves; agrees with VI to |
| prioritized sweeping, cold | 226,050 backups | 2.6× worse — the heap is pure overhead |
| prioritized sweeping, repairing one painted hazard | 4,487 backups | vs 21,600 for warm Gauss–Seidel |
Then the check that matters: does the value function mean anything? at the start cell is . Twenty thousand seeded rollouts of the greedy policy, sampling transitions from the same sparse rows, average — within 2%, with a mean journey of 45.5 steps. Monte Carlo meeting dynamic programming on the page, which is the only way to be sure that the number the solver printed is the number the robot will actually experience.
And the sweep across noise, which is the chapter's thesis in five rows:
| mean steps to dock | |||||
| VI sweeps to |
Noise costs value, costs steps, and costs sweeps. Nothing in the policy was "made safe"; the expectation did all of it.
What this chapter is not
Not reinforcement learning. Q-learning, SARSA and actor–critic solve these Bellman equations with and unknown, learning from samples instead of from a model. That is a genuinely different problem — exploration becomes an issue, and convergence proofs get much harder — and it belongs to Chapter 25. What is worth carrying forward is that the equation being solved is the one above, unchanged.
Not continuous state. Discretizing a 3-DOF pose at any useful resolution is already millions of states, and adding velocities makes it hopeless. The answer in practice is not a finer grid but a different algorithm: sample trajectories from the current state, evaluate them, act, throw them away, repeat. That is Chapter 23, where the value function of this chapter reappears as the terminal cost that makes a short horizon behave like a long one.
Not observable. Here is the bridge, and it is worth being blunt about. Take the Policy Painter's converged policy — provably optimal, arrows correct in every cell — and kidnap Rusty. The policy is still optimal. It is also useless, because requires , and after a kidnapping the robot has a belief, not a state. Nothing in this chapter's machinery has an input port for a distribution.
Chapter 22 fixes this in the only way the mathematics allows: promote the belief to the state. The belief-MDP is a genuine MDP over a continuous, high-dimensional space, the Bellman equation is unchanged, and every algorithm here still applies in principle. Whether they apply in practice is what makes that chapter hard. One of them — QMDP — is nothing more than this chapter's averaged against the belief, , so keep the -values; you will need them in about twenty pages.
Exercises
- Foundation exerciseDifficulty 1 of 3The max inequality, and when it is tight
Prove for finite index sets, and exhibit vectors where it holds with equality. Then find vectors where the left side is and the right side is as large as you like, and say in one sentence what that means for the tightness of the contraction bound on a real gridworld.
- Foundation exerciseDifficulty 2 of 3The hallway in closed form
For the four-cell hallway with success probability (and
lungedisabled), derive in closed form and verify the numbers. Then: at whatlungecost does the optimal policy start preferring it, and is the answer the same at every state? Finally, predict how many synchronous sweeps value iteration needs to reach using the geometric bound with the effective contraction modulus , and compare with the measured count. - Foundation exerciseDifficulty 3 of 3Why undiscounted is harder
Build two four-state undiscounted goal-absorbing MDPs, both starting value iteration from . On the first, value iteration diverges: some state's value runs to . On the second it converges to a finite fixed point whose optimal policy never reaches the goal. Say which of the two conditions in the SSP box each one violates. (Hint for the second: one free action is enough.) Then repair each in two ways — a strictly positive cost on every action, and a discount — and say what each repair changes about the optimal policy, not merely about the solver's ability to find it.
- Conceptual exerciseDifficulty 2 of 3Predict the cliff
In the Cliff Run, before committing your prediction: estimate for the default geometry using only the two closed forms in the derivation and a calculator. Then use the widget's guess slider to commit, reveal, and bisect with the slip slider to find where the arrow at the start actually flips. Finally — and this is the interesting part — explain why the slip at which realized runs start preferring the detour is noticeably higher than , and what that gap is made of.
- Conceptual exerciseDifficulty 2 of 3Make the discount change the topology
Using the Policy Painter, construct a painting in which lowering does not merely make the arrows lazier but routes the robot through a different doorway. Record both arrow fields and both values at the start cell, and explain the mechanism in terms of the effective horizon and the two routes' lengths. Then predict, before checking, whether raising the slip makes the effect appear at a higher or lower .
- Practical exerciseDifficulty 2 of 3Prioritized sweeping, and when it loses
Implement
prioritized_sweepingin Rust with aBinaryHeapkeyed on the Bellman residual and the priority rule from the algorithm box. Reproduce both halves of this chapter's measurement on the Apartment: that it loses to Gauss–Seidel on a cold solve (226k backups vs 86k), and that it wins by roughly 5× when repairing a single changed reward cell. Plot backups against for both, and explain the crossover in terms of what fraction of states have a nonzero residual. - Practical exerciseDifficulty 3 of 3The wave-front planner, generated
Add an SSP mode () to the crate with proper-policy detection: build the predecessor graph with
petgraph, run a reverse traversal from the goal set, and flag every state that cannot reach a goal instead of letting its value run to . Then set the slip to zero and assert that equals, cell for cell, the distance field produced by Chapter 20's wave-front planner on the same map. If the assert fails, the most likely culprit is diagonal cost: check that you charged .
References
- Bellman, R. (1957) A Markovian Decision Process. Indiana University Mathematics Journal 6(4), 679–684.doi:10.1512/iumj.1957.6.56038 (opens in a new tab)
The five-page paper that named the object and wrote down the optimality equation this whole chapter revolves around.
- Howard, R. A. (1960) Dynamic Programming and Markov Processes. MIT Press.link to Dynamic Programming and Markov Processes (opens in a new tab)
Policy iteration's origin, still the clearest account of why evaluate-then-improve terminates. The algorithm in this chapter's second box is Howard's, essentially unmodified.
- Bertsekas, D. P. and Tsitsiklis, J. N. (1991) An Analysis of Stochastic Shortest Path Problems. Mathematics of Operations Research 16(3), 580–595.doi:10.1287/moor.16.3.580 (opens in a new tab)
The proper-policy conditions in the SSP box, proved. This is the reference for why undiscounted navigation MDPs are well posed — and exactly when they are not.
- Moore, A. W. and Atkeson, C. G. (1993) Prioritized sweeping: Reinforcement learning with less data and less time. Machine Learning 13(1), 103–130.doi:10.1007/BF00993104 (opens in a new tab)
The priority rule implemented in this chapter's third algorithm box, including the argument for keying on how much of a change can reach each predecessor.
- Barto, A. G., Bradtke, S. J., and Singh, S. P. (1995) Learning to act using real-time dynamic programming. Artificial Intelligence 72(1), 81–138.doi:10.1016/0004-3702(94)00011-O (opens in a new tab)
Asynchronous DP made into a control architecture: back up only the states you actually visit, and act on the half-converged value function. The formal justification for what the Policy Painter does every frame.
- Thrun, S., Burgard, W., and Fox, D. (2005) Probabilistic Robotics. MIT Press.link to Probabilistic Robotics (opens in a new tab)
Chapter 15 is this chapter's baseline: the MDP framing, the payoff notation, and the MDP_value_iteration algorithm of Table 15.1, which the first algorithm box reproduces with a stopping rule added.
- Kurniawati, H. (2022) Partially Observable Markov Decision Processes and Robotics. Annual Review of Control, Robotics, and Autonomous Systems 5, 253–277.doi:10.1146/annurev-control-042920-092451 (opens in a new tab)
The modern survey of what happens when you drop this chapter's full-observability assumption. Read the introduction now for the bridge to Chapter 22; read the rest after it.
- Akella, P., Dixit, A., Ahmadi, M., Lindemann, L., Chapman, M. P., Pappas, G. J., Ames, A. D., and Burdick, J. W. (2025) Risk-Aware Robotics: Tail Risk Measures in Planning, Control, and Verification. IEEE Control Systems 45(4), 46–78.doi:10.1109/MCS.2025.3577050 (opens in a new tab)
What to do when maximizing the expectation is not what you meant — CVaR and other tail measures, and what they cost you in tractability. The honest answer to the Cliff Run's caveat about individual runs.
