Skip to main content

use_random_walk/
lib.rs

1#![forbid(unsafe_code)]
2//! Repeatable one-dimensional random walk helpers.
3//!
4//! The walk uses `SimulationSeed` to derive deterministic left and right moves
5//! so repeated runs with the same configuration produce the same path.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_random_walk::{random_walk, summarize_walk, RandomWalkConfig};
11//! use use_seed::SimulationSeed;
12//!
13//! let path = random_walk(RandomWalkConfig {
14//!     start: 0.0,
15//!     step_size: 1.0,
16//!     steps: 4,
17//!     seed: SimulationSeed::new(5),
18//! })
19//! .unwrap();
20//! let summary = summarize_walk(&path).unwrap();
21//!
22//! assert_eq!(path.len(), 5);
23//! assert_eq!(summary.final_position, *path.last().unwrap());
24//! ```
25
26use 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}