1#![forbid(unsafe_code)]
2use use_seed::SimulationSeed;
27
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct RandomWalkConfig {
30 pub start: f64,
31 pub step_size: f64,
32 pub steps: usize,
33 pub seed: SimulationSeed,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct RandomWalkSummary {
38 pub final_position: f64,
39 pub min_position: f64,
40 pub max_position: f64,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum RandomWalkError {
45 InvalidStart,
46 InvalidStepSize,
47 NonFinitePosition,
48}
49
50pub fn random_walk(config: RandomWalkConfig) -> Result<Vec<f64>, RandomWalkError> {
51 if !config.start.is_finite() {
52 return Err(RandomWalkError::InvalidStart);
53 }
54
55 if !config.step_size.is_finite() || config.step_size < 0.0 {
56 return Err(RandomWalkError::InvalidStepSize);
57 }
58
59 let mut current = config.start;
60 let mut path = Vec::with_capacity(config.steps + 1);
61 path.push(current);
62
63 for step_index in 0..config.steps {
64 let direction = if config.seed.mix(step_index + 1).to_unit_f64() < 0.5 {
65 -1.0
66 } else {
67 1.0
68 };
69
70 let next = current + direction * config.step_size;
71 if !next.is_finite() {
72 return Err(RandomWalkError::NonFinitePosition);
73 }
74
75 path.push(next);
76 current = next;
77 }
78
79 Ok(path)
80}
81
82pub fn summarize_walk(path: &[f64]) -> Option<RandomWalkSummary> {
83 if path.is_empty() || path.iter().any(|value| !value.is_finite()) {
84 return None;
85 }
86
87 let mut min_position = path[0];
88 let mut max_position = path[0];
89 for value in path.iter().copied().skip(1) {
90 if value < min_position {
91 min_position = value;
92 }
93 if value > max_position {
94 max_position = value;
95 }
96 }
97
98 Some(RandomWalkSummary {
99 final_position: *path.last()?,
100 min_position,
101 max_position,
102 })
103}
104
105#[cfg(test)]
106mod tests {
107 use super::{RandomWalkConfig, RandomWalkError, random_walk, summarize_walk};
108 use use_seed::SimulationSeed;
109
110 #[test]
111 fn produces_repeatable_paths() {
112 let config = RandomWalkConfig {
113 start: 0.0,
114 step_size: 1.0,
115 steps: 5,
116 seed: SimulationSeed::new(4),
117 };
118
119 let first = random_walk(config).unwrap();
120 let second = random_walk(config).unwrap();
121
122 assert_eq!(first, second);
123 assert_eq!(first.len(), 6);
124 assert!(
125 first
126 .windows(2)
127 .all(|pair| (pair[1] - pair[0]).abs() == 1.0)
128 );
129 }
130
131 #[test]
132 fn summarizes_walk_positions() {
133 let path = random_walk(RandomWalkConfig {
134 start: 0.0,
135 step_size: 0.5,
136 steps: 4,
137 seed: SimulationSeed::new(7),
138 })
139 .unwrap();
140 let summary = summarize_walk(&path).unwrap();
141
142 assert_eq!(summary.final_position, *path.last().unwrap());
143 assert!(summary.min_position <= summary.max_position);
144 }
145
146 #[test]
147 fn handles_zero_steps() {
148 assert_eq!(
149 random_walk(RandomWalkConfig {
150 start: 3.0,
151 step_size: 1.0,
152 steps: 0,
153 seed: SimulationSeed::new(1),
154 })
155 .unwrap(),
156 vec![3.0]
157 );
158 }
159
160 #[test]
161 fn rejects_invalid_configuration() {
162 assert_eq!(
163 random_walk(RandomWalkConfig {
164 start: f64::NAN,
165 step_size: 1.0,
166 steps: 1,
167 seed: SimulationSeed::new(1),
168 }),
169 Err(RandomWalkError::InvalidStart)
170 );
171 assert_eq!(
172 random_walk(RandomWalkConfig {
173 start: 0.0,
174 step_size: -1.0,
175 steps: 1,
176 seed: SimulationSeed::new(1),
177 }),
178 Err(RandomWalkError::InvalidStepSize)
179 );
180 assert_eq!(summarize_walk(&[]), None);
181 }
182}