From 401ad5f7501409cc226609ffa0265d1ee63feefc Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Tue, 24 Feb 2026 12:45:04 -0800 Subject: [PATCH] =?UTF-8?q?docs(edu):=20write=20markov=20=C2=A74=20weather?= =?UTF-8?q?=20model=20exercise=20[257a2a]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add full content for Exercise 1 — Weather Model: - Setup instructions (cargo new, cargo add rand) - Starter code skeleton with todo!() bodies - Step-by-step hints: index conversion, weighted random draw, simulate loop, running with the 2-state matrix, comparing empirical frequencies to the stationary distribution (2/3, 1/3) - Collapsed reference solution Co-Authored-By: Claude Sonnet 4.6 --- edu/markov.md | 162 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 1 deletion(-) diff --git a/edu/markov.md b/edu/markov.md index 74ea5c6..6b227d1 100644 --- a/edu/markov.md +++ b/edu/markov.md @@ -127,7 +127,23 @@ P^2 = [[0.8*0.8 + 0.2*0.4, 0.8*0.2 + 0.2*0.6], **Goal:** Build a two-state Markov chain in Rust that models daily weather as either `Sunny` or `Rainy`, driven by a transition matrix, and simulate 30 days of weather. +#### Setup + +Create a new Cargo binary project and add the `rand` crate: + +```sh +cargo new weather-chain +cd weather-chain +cargo add rand +``` + +#### 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 and write `main`. + ```rust +use rand::Rng; + #[derive(Debug, Clone, Copy, PartialEq)] enum Weather { Sunny, Rainy } @@ -140,9 +156,153 @@ impl WeatherChain { fn step(&self, current: Weather, rng: &mut impl Rng) -> Weather { todo!() } fn simulate(&self, start: Weather, steps: usize, rng: &mut impl Rng) -> Vec { todo!() } } + +fn main() { todo!() } +``` + +#### Step 1 — Index conversion + +`step` needs to index into `self.transition` using the current state, and later convert an index back to a `Weather` value. Add two helper methods to the `Weather` enum: + +- `fn index(self) -> usize` — returns `0` for `Sunny`, `1` for `Rainy` +- `fn from_index(i: usize) -> Self` — returns `Sunny` for `0`, `Rainy` for anything else + +A simple `match` expression handles both. These are the only tools you need to bridge the enum and the matrix. + +#### Step 2 — Implement `WeatherChain::step` + +`step` must sample the next state from the probability row for the current state. The technique is a **cumulative-probability walk**: + +1. Retrieve the transition row: `let row = self.transition[current.index()];` +2. Draw a uniform float in [0, 1): `let r: f64 = rng.gen();` +3. Walk the row, accumulating probability. When the running total first exceeds `r`, return that state. + +For the two-state case this reduces to a single comparison — if `r < row[0]` return `Sunny`, otherwise return `Rainy` — but implementing the loop works for any number of states and is worth practising. + +Add a fallback `Weather::from_index(row.len() - 1)` after the loop to satisfy the compiler; floating-point rounding can in rare cases leave the loop without returning. + +#### Step 3 — Implement `WeatherChain::simulate` + +`simulate` runs the chain for `steps` transitions, collecting every state visited including the start: + +1. Allocate a `Vec` with capacity `steps + 1`. +2. Push `start` and set `current = start`. +3. Loop `steps` times: call `self.step(current, rng)`, update `current`, and push. +4. Return the `Vec`. + +The returned slice will have length `steps + 1` (the initial state plus one state per step). + +#### Step 4 — Run the simulation + +In `main`, create a `WeatherChain` with the two-state matrix from Section 3: + +``` +Sunny [0.8, 0.2] +Rainy [0.4, 0.6] +``` + +Seed a repeatable RNG with `rand::rngs::SmallRng::seed_from_u64(42)` (add `use rand::SeedableRng;`). Simulate 30 steps starting from `Sunny`, print the resulting sequence, and count how many days were sunny vs rainy. + +Expected output structure (exact numbers vary by seed): + +``` +[Sunny, Sunny, Rainy, Sunny, ...] +Sunny days: 21 (67.7%) +Rainy days: 10 (32.3%) +``` + +#### Step 5 — Compare to the stationary distribution + +The **stationary distribution** π satisfies π = πP, meaning once the chain reaches it, the distribution no longer changes. For this matrix, solve the two equations: + +``` +π₀ = 0.8·π₀ + 0.4·π₁ +π₁ = 0.2·π₀ + 0.6·π₁ +π₀ + π₁ = 1 +``` + +The first equation simplifies to `0.2·π₀ = 0.4·π₁`, giving `π₀ = 2·π₁`. Substituting into the normalisation constraint: `π₀ = 2/3 ≈ 66.7%`, `π₁ = 1/3 ≈ 33.3%`. + +Print the stationary percentages alongside your simulated counts. With only 31 data points the match will be rough; re-run with 1 000 or 10 000 steps to see the empirical frequencies converge. + +#### Reference Solution + +
+Show full solution + +```rust +use rand::{Rng, SeedableRng, rngs::SmallRng}; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Weather { Sunny, Rainy } + +impl Weather { + fn index(self) -> usize { + match self { + Weather::Sunny => 0, + Weather::Rainy => 1, + } + } + + fn from_index(i: usize) -> Self { + match i { + 0 => Weather::Sunny, + _ => Weather::Rainy, + } + } +} + +struct WeatherChain { + /// transition[current][next] = probability + transition: [[f64; 2]; 2], +} + +impl WeatherChain { + fn step(&self, current: Weather, rng: &mut impl Rng) -> Weather { + let row = self.transition[current.index()]; + let r: f64 = rng.gen(); + let mut cumulative = 0.0; + for (i, &prob) in row.iter().enumerate() { + cumulative += prob; + if r < cumulative { + return Weather::from_index(i); + } + } + Weather::from_index(row.len() - 1) + } + + fn simulate(&self, start: Weather, steps: usize, rng: &mut impl Rng) -> Vec { + let mut states = Vec::with_capacity(steps + 1); + states.push(start); + let mut current = start; + for _ in 0..steps { + current = self.step(current, rng); + states.push(current); + } + states + } +} + +fn main() { + let mut rng = SmallRng::seed_from_u64(42); + let chain = WeatherChain { + transition: [[0.8, 0.2], [0.4, 0.6]], + }; + + let states = chain.simulate(Weather::Sunny, 30, &mut rng); + println!("{:?}", states); + + let total = states.len() as f64; + let sunny = states.iter().filter(|&&s| s == Weather::Sunny).count(); + let rainy = states.len() - sunny; + + println!("Sunny days: {} ({:.1}%)", sunny, 100.0 * sunny as f64 / total); + println!("Rainy days: {} ({:.1}%)", rainy, 100.0 * rainy as f64 / total); + println!("Stationary: Sunny ≈ 66.7%, Rainy ≈ 33.3%"); +} ``` -> 🚧 This section is a stub — see nbd ticket `257a2a` +
---