title = "Markov exercise: Simulating a Random Walk (Rust)"
priority = 7
status = "archived"
ticket_type = "task"
dependencies = []
+++
Write Section 5 of edu/markov.md: Exercise 2 — Simulating a Random Walk\n\nLearning objectives:\n- Model a countably-finite state space (bounded integers) in Rust\n- Implement reflecting boundary conditions\n- Aggregate results from many trials into a histogram\n\nContent to produce:\n- Setup instructions (reuse or extend Exercise 1 project)\n- Step-by-step hints:\n 1. Define RandomWalk with min, max, prob_right fields\n 2. Implement step with boundary reflection (clamp or reverse)\n 3. Implement histogram by running simulate() many times\n 4. Print histogram as ASCII bar chart\n 5. Observe convergence toward uniform (symmetric walk) or skewed (asymmetric)\n- Full reference solution\n\nTarget: replace the stub in edu/markov.md §5
**Goal:** Implement a one-dimensional random walk on the integers (states −*N* … +*N*) with reflecting boundaries, then measure the empirical distribution of positions after *T* steps.
A **random walk** is one of the simplest Markov chains: the state is a single integer position, and at each step the walker moves left or right according to a fixed probability. Adding **reflecting boundaries** means the walker cannot escape the interval — a step that would leave the interval is clamped to the nearest boundary.
- Visualise the distribution as an ASCII bar chart
- Observe how asymmetric step probabilities skew the stationary distribution
#### Setup
You can extend the `weather-chain` project from Exercise 1, or create a fresh binary:
```sh
cargo new random-walk
cd random-walk
cargo add rand
```
You will also need `use std::collections::HashMap;` at the top of `src/main.rs`.
#### Starter Code
Replace the contents of `src/main.rs` with the following skeleton. Do not change the struct layout or function signatures — your task is to fill in the `todo!()` bodies.
```rust
use rand::Rng;
use std::collections::HashMap;
struct RandomWalk {
min: i32,
max: i32,
/// prob_right[i] = probability of stepping right from position i
/// prob_right[i] = probability of stepping right from position `min + i as i32`
The `prob_right` vec is indexed from 0, but positions range from `min` to `max`. Add two helpers to `RandomWalk`:
- `fn new(min: i32, max: i32, p: f64) -> Self` — creates a **uniform** walk where every position has the same step probability `p`. Fill `prob_right` with `vec![p; (max - min + 1) as usize]`.
- `fn idx(&self, pos: i32) -> usize` — converts a position to a vec index: `(pos - self.min) as usize`. Use this everywhere you index into `prob_right`.
#### Step 2 — Implement `RandomWalk::step`
`step` must sample the next position:
1. Draw a uniform float `r` in [0, 1): `let r: f64 = rng.gen();`
2. Look up the step probability: `let p = self.prob_right[self.idx(pos)];`
3. Choose direction: if `r < p`, move right (`pos + 1`); otherwise move left (`pos - 1`)
4. Apply the **reflecting boundary** by clamping: `moved.clamp(self.min, self.max)`
```rust
let moved = if r <p{pos+1}else{pos-1};
moved.clamp(self.min, self.max)
```
Clamping is the simplest reflecting rule: a step that would leave the interval is silently held at the boundary. An alternative is *strict reflection* (a step right from `max` lands at `max - 1`), but clamping is sufficient here.
#### Step 3 — Implement `RandomWalk::simulate` and `RandomWalk::histogram`
`simulate` runs the chain for `steps` transitions and collects every position visited:
`histogram` runs many independent trials and records where each trial ends:
1. Allocate an empty `HashMap<i32, usize>`.
2. Loop `trials` times: call `self.simulate(start, steps, rng)`, extract `*positions.last().unwrap()`, and increment its count with `*hist.entry(pos).or_insert(0) += 1`.
3. Return the map.
The histogram answers: *after `steps` steps starting from `start`, what fraction of the time does the walker end at each position?*
#### Step 4 — Print an ASCII bar chart
Write `print_histogram` to display the distribution. For each position from `min` to `max`:
1. Look up `count = hist.get(&pos).copied().unwrap_or(0)`.
2. Scale to a bar: `bar_len = count * bar_width / max_count` (use `bar_width = 40` and `max_count = hist.values().copied().max().unwrap_or(1)`).
3. Print the position, a bar of `#` characters, and the percentage.
```rust
let bar: String = "#".repeat(bar_len);
println!("{:4}: {:40} ({:.1}%)", pos, bar, 100.0 * count as f64 / trials as f64);
```
#### Step 5 — Compare symmetric vs asymmetric walks
In `main`, run two experiments. Seed a repeatable RNG with `SmallRng::seed_from_u64(42)` (add `use rand::{SeedableRng, rngs::SmallRng};`).
**Symmetric walk (p = 0.5, range −5 … 5):**
```rust
let walk = RandomWalk::new(-5, 5, 0.5);
let hist = walk.histogram(0, 200, 10_000, &mut rng);
print_histogram(&hist, -5, 5, 10_000);
```
**Asymmetric walk (p = 0.7, same range):**
```rust
let walk = RandomWalk::new(-5, 5, 0.7);
let hist = walk.histogram(0, 200, 10_000, &mut rng);
print_histogram(&hist, -5, 5, 10_000);
```
Observe:
- **Symmetric (p = 0.5):** after enough steps the distribution is approximately **uniform** — each of the 11 positions appears with roughly 9% frequency. This is the stationary distribution for a symmetric walk with clamping boundaries.
- **Asymmetric (p = 0.7):** the walker drifts toward the right boundary; positions near +5 accumulate much higher frequencies than those near −5. The stationary distribution is geometric, rising steeply toward `max`.
Try lowering `steps` from 200 to 10 and observe that the distribution has not yet converged — it is still concentrated near `start`. This illustrates the **mixing time** of the chain.
#### Reference Solution
<details>
<summary>Show full solution</summary>
```rust
use rand::{Rng, SeedableRng, rngs::SmallRng};
use std::collections::HashMap;
struct RandomWalk {
min: i32,
max: i32,
/// prob_right[i] = probability of stepping right from position `min + i as i32`
The symmetric walk converges to a nearly flat distribution — each position visited roughly 1/11 ≈ 9.1% of the time. The asymmetric walk piles up at the right boundary, with positions +3 … +5 capturing the bulk of the probability mass.