Problem statement: Suppose that n random walkers, starting in the center of an n-by-n grid, move one step at a time, choosing to go left, right, up, or down with equal probability at each step. Write a program to help formulate and test a hypothesis about the number of steps taken before all cells are touched.
This is one of my self-imposed challenges in Rust to become better at it. The problem was taken from Sedgewick Exercise 1.4.36.
Here is my code:
use clap::Parser;
use plotly::common::Title;
use plotly::layout::{Axis, Layout};
use plotly::{HeatMap, Plot};
use rand::rngs::ThreadRng;
use rand::Rng;
#[derive(Debug, Parser)]
struct Arguments {
#[arg(index = 1)]
number_of_trials: usize,
#[arg(index = 2)]
walker_number_range_start: usize,
#[arg(index = 3)]
walker_number_range_end: usize,
#[arg(index = 4)]
walker_number_range_step: usize,
#[arg(index = 5)]
grid_size_range_start: usize,
#[arg(index = 6)]
grid_size_range_end: usize,
#[arg(index = 7)]
grid_size_range_step: usize,
#[arg(index = 8)]
grid_type: String,
}
fn main() -> Result<(), String> {
let arguments = Arguments::parse();
let number_of_trials = arguments.number_of_trials;
let walker_number_range_start = arguments.walker_number_range_start;
let walker_number_range_end = arguments.walker_number_range_end;
let walker_number_range_step = arguments.walker_number_range_step;
let grid_size_range_start = arguments.grid_size_range_start;
let grid_size_range_end = arguments.grid_size_range_end;
let grid_size_range_step = arguments.grid_size_range_step;
let grid_type = arguments.grid_type;
let mut rng = rand::thread_rng();
let walker_number_range = (
walker_number_range_start,
walker_number_range_end,
walker_number_range_step,
);
let grid_size_range = (
grid_size_range_start,
grid_size_range_end,
grid_size_range_step,
);
let heat_map = run_parametric_experiment(
number_of_trials,
walker_number_range,
grid_size_range,
&grid_type,
&mut rng,
)?;
let heat_map_row_indices: Vec<usize> = (walker_number_range.0..walker_number_range.1)
.step_by(walker_number_range.2)
.collect();
let heat_map_column_indices: Vec<usize> = (grid_size_range.0..grid_size_range.1)
.step_by(grid_size_range.2)
.collect();
let mut plot = Plot::new();
let trace = HeatMap::new(heat_map_row_indices, heat_map_column_indices, heat_map);
let layout = Layout::new()
.x_axis(Axis::new().title(Title::from("Grid Size")))
.y_axis(Axis::new().title(Title::from("Walker Number")));
plot.add_trace(trace);
plot.set_layout(layout);
plot.show();
Ok(())
}
#[derive(Clone, Debug)]
struct Walker {
x: usize,
y: usize,
grid_size: usize,
grid_type: String,
}
impl Walker {
fn new(x: usize, y: usize, grid_size: usize, grid_type: &str) -> Result<Self, String> {
if grid_size < 1 {
return Err("Grid size must be at least 1".to_string());
}
if x >= grid_size {
return Err(format!(
"X coordinate: {} greater than or equal to grid size: {}",
x, grid_size
));
} else if y >= grid_size {
return Err(format!(
"Y coordinate: {} greater than or equal to grid size: {}",
y, grid_size
));
}
if grid_type != "plane" && grid_type != "torus" {
return Err("Defined grid types are plane and torus".to_string());
}
Ok(Walker {
x,
y,
grid_size,
grid_type: grid_type.to_string(),
})
}
fn get_x_coordinate(&self) -> usize {
self.x
}
fn get_y_coordinate(&self) -> usize {
self.y
}
fn walk_on_plane(&mut self, rng: &mut ThreadRng) {
let r = rng.gen_range(0.0..1.0);
// Unit case
if self.grid_size == 1 {
}
// Corner cases
else if self.x == 0 && self.y == 0 {
if r < 1.0 / 2.0 {
self.x += 1;
} else {
self.y += 1;
}
} else if self.x == 0 && self.y == self.grid_size - 1 {
if r < 1.0 / 2.0 {
self.x += 1;
} else {
self.y -= 1;
}
} else if self.x == self.grid_size - 1 && self.y == self.grid_size - 1 {
if r < 1.0 / 2.0 {
self.x -= 1;
} else {
self.y -= 1;
}
} else if self.x == self.grid_size - 1 && self.y == 0 {
if r < 1.0 / 2.0 {
self.x -= 1;
} else {
self.y += 1;
}
}
// Edge cases
else if self.x == 0 {
if r < 1.0 / 3.0 {
self.x += 1;
} else if r < 2.0 / 3.0 {
self.y += 1;
} else {
self.y -= 1;
}
} else if self.x == self.grid_size - 1 {
if r < 1.0 / 3.0 {
self.x -= 1;
} else if r < 2.0 / 3.0 {
self.y += 1;
} else {
self.y -= 1;
}
} else if self.y == 0 {
if r < 1.0 / 3.0 {
self.x += 1;
} else if r < 2.0 / 3.0 {
self.x -= 1;
} else {
self.y += 1;
}
} else if self.y == self.grid_size - 1 {
if r < 1.0 / 3.0 {
self.x += 1;
} else if r < 2.0 / 3.0 {
self.x -= 1;
} else {
self.y -= 1;
}
// Regular case
} else if r < 1.0 / 4.0 {
self.x += 1;
} else if r < 2.0 / 4.0 {
self.x -= 1;
} else if r < 3.0 / 4.0 {
self.y += 1;
} else {
self.y -= 1;
}
}
fn walk_on_torus(&mut self, rng: &mut ThreadRng) {
let r = rng.gen_range(0.0..1.0);
if r < 1.0 / 4.0 {
self.x = (((self.x + 1) % self.grid_size) + self.grid_size) % self.grid_size;
} else if r < 2.0 / 4.0 {
if self.x == 0 {
self.x = self.grid_size - 1;
} else {
self.x = (((self.x - 1) % self.grid_size) + self.grid_size) % self.grid_size;
}
} else if r < 3.0 / 4.0 {
self.y = (((self.y + 1) % self.grid_size) + self.grid_size) % self.grid_size;
} else if self.y == 0 {
self.y = self.grid_size - 1;
} else {
self.y = (((self.y - 1) % self.grid_size) + self.grid_size) % self.grid_size;
}
}
fn walk(&mut self, rng: &mut ThreadRng) {
if self.grid_type == "plane" {
self.walk_on_plane(rng);
} else if self.grid_type == "torus" {
self.walk_on_torus(rng);
}
}
}
fn simulate_n_walkers_1_time(
walker_number: usize,
grid_size: usize,
grid_type: &str,
rng: &mut ThreadRng,
) -> Result<usize, String> {
if walker_number < 1 {
return Err("Number of walkers must be at least 1".to_string());
}
let x: usize = grid_size / 2;
let y: usize = grid_size / 2;
let mut walkers = vec![Walker::new(x, y, grid_size, grid_type)?; walker_number];
let mut grid = vec![vec![false; grid_size]; grid_size];
grid[x][y] = true;
let total_grid_cell_number: usize = grid_size * grid_size;
let mut walked_grid_cell_number: usize = 1;
let mut number_of_steps: usize = 0;
while walked_grid_cell_number < total_grid_cell_number {
number_of_steps += 1;
for walker in walkers.iter_mut().take(walker_number) {
walker.walk(rng);
let x_coordinate = walker.get_x_coordinate();
let y_coordinate = walker.get_y_coordinate();
if !grid[x_coordinate][y_coordinate] {
grid[x_coordinate][y_coordinate] = true;
walked_grid_cell_number += 1;
}
}
}
Ok(number_of_steps)
}
fn simulate_m_walkers_n_times(
number_of_trials: usize,
walker_number: usize,
grid_size: usize,
grid_type: &str,
rng: &mut ThreadRng,
) -> Result<usize, String> {
if number_of_trials < 1 {
return Err("Number of trials must be at least 1".to_string());
}
let mut number_of_steps = 0;
for _ in 0..number_of_trials {
number_of_steps += simulate_n_walkers_1_time(walker_number, grid_size, grid_type, rng)?;
}
Ok(number_of_steps / number_of_trials)
}
fn run_parametric_experiment(
number_of_trials: usize,
walker_number_range: (usize, usize, usize),
grid_size_range: (usize, usize, usize),
grid_type: &str,
rng: &mut ThreadRng,
) -> Result<Vec<Vec<usize>>, String> {
let number_of_walker_numbers =
(walker_number_range.1 - walker_number_range.0) / walker_number_range.2;
let number_of_grid_sizes = (grid_size_range.1 - grid_size_range.0) / grid_size_range.2;
let mut heat_map = vec![vec![0; number_of_walker_numbers]; number_of_grid_sizes];
let walker_number_iterator_start = walker_number_range.0;
let walker_number_iterator_end =
number_of_walker_numbers * walker_number_range.2 + walker_number_range.0;
let walker_number_iterator =
(walker_number_iterator_start..walker_number_iterator_end).step_by(walker_number_range.2);
let grid_size_iterator_start = grid_size_range.0;
let grid_size_iterator_end = number_of_grid_sizes * grid_size_range.2 + grid_size_range.0;
let grid_size_iterator =
(grid_size_iterator_start..grid_size_iterator_end).step_by(grid_size_range.2);
for (i, walker_number) in walker_number_iterator.enumerate() {
for (j, grid_number) in grid_size_iterator.clone().enumerate() {
heat_map[i][j] = simulate_m_walkers_n_times(
number_of_trials,
walker_number,
grid_number,
grid_type,
rng,
)?;
}
}
Ok(heat_map)
}
Example input:
cargo run --release 1000 11 111 10 11 111 10 torus
Example output:
Is there any way that I can improve my code?