1#[derive(Debug, Clone)]
35pub struct ReplayBuffer {
36 obs_dim: usize,
37 capacity: usize,
38 len: usize,
39 write_idx: usize,
40
41 observations: Vec<f32>,
42 actions: Vec<i64>,
43 rewards: Vec<f32>,
44 next_observations: Vec<f32>,
45 dones: Vec<bool>,
46}
47
48impl ReplayBuffer {
49 pub fn new(capacity: usize, obs_dim: usize) -> Self {
59 assert!(capacity > 0, "ReplayBuffer capacity must be > 0");
60 assert!(obs_dim > 0, "ReplayBuffer obs_dim must be > 0");
61
62 Self {
63 obs_dim,
64 capacity,
65 len: 0,
66 write_idx: 0,
67 observations: vec![0.0; capacity * obs_dim],
68 actions: vec![0; capacity],
69 rewards: vec![0.0; capacity],
70 next_observations: vec![0.0; capacity * obs_dim],
71 dones: vec![false; capacity],
72 }
73 }
74
75 pub fn push(&mut self, obs: &[f32], action: i64, reward: f32, next_obs: &[f32], done: bool) {
92 debug_assert_eq!(
93 obs.len(),
94 self.obs_dim,
95 "ReplayBuffer::push: obs.len() ({}) != obs_dim ({})",
96 obs.len(),
97 self.obs_dim
98 );
99 debug_assert_eq!(
100 next_obs.len(),
101 self.obs_dim,
102 "ReplayBuffer::push: next_obs.len() ({}) != obs_dim ({})",
103 next_obs.len(),
104 self.obs_dim
105 );
106
107 let start = self.write_idx * self.obs_dim;
108 let end = start + self.obs_dim;
109 self.observations[start..end].copy_from_slice(obs);
110 self.next_observations[start..end].copy_from_slice(next_obs);
111 self.actions[self.write_idx] = action;
112 self.rewards[self.write_idx] = reward;
113 self.dones[self.write_idx] = done;
114
115 self.write_idx = (self.write_idx + 1) % self.capacity;
116 if self.len < self.capacity {
117 self.len += 1;
118 }
119 }
120
121 pub fn len(&self) -> usize {
123 self.len
124 }
125
126 pub fn is_empty(&self) -> bool {
128 self.len == 0
129 }
130
131 pub fn capacity(&self) -> usize {
133 self.capacity
134 }
135
136 pub fn obs_dim(&self) -> usize {
138 self.obs_dim
139 }
140
141 pub fn is_ready(&self, min_size: usize) -> bool {
147 self.len >= min_size
148 }
149
150 pub(super) fn read_into(
157 &self,
158 i: usize,
159 obs_out: &mut [f32],
160 next_obs_out: &mut [f32],
161 ) -> (i64, f32, bool) {
162 assert!(i < self.len, "ReplayBuffer::read_into: i ({}) >= len ({})", i, self.len);
163 assert_eq!(obs_out.len(), self.obs_dim);
164 assert_eq!(next_obs_out.len(), self.obs_dim);
165
166 let start = i * self.obs_dim;
167 let end = start + self.obs_dim;
168 obs_out.copy_from_slice(&self.observations[start..end]);
169 next_obs_out.copy_from_slice(&self.next_observations[start..end]);
170 (self.actions[i], self.rewards[i], self.dones[i])
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_push_and_len() {
180 let mut buf = ReplayBuffer::new(8, 3);
181 assert!(buf.is_empty());
182 assert_eq!(buf.capacity(), 8);
183 assert_eq!(buf.obs_dim(), 3);
184
185 buf.push(&[1.0, 2.0, 3.0], 1, 0.5, &[4.0, 5.0, 6.0], false);
186 assert_eq!(buf.len(), 1);
187 assert!(!buf.is_empty());
188
189 for i in 0..5 {
190 buf.push(&[i as f32; 3], i as i64, i as f32, &[(i + 1) as f32; 3], false);
191 }
192 assert_eq!(buf.len(), 6);
193 }
194
195 #[test]
196 fn test_roundtrip_values() {
197 let mut buf = ReplayBuffer::new(4, 2);
199 buf.push(&[1.0, 2.0], 0, 10.0, &[3.0, 4.0], false);
200 buf.push(&[5.0, 6.0], 1, 20.0, &[7.0, 8.0], true);
201
202 let mut obs = [0.0f32; 2];
203 let mut next_obs = [0.0f32; 2];
204 let (a, r, d) = buf.read_into(0, &mut obs, &mut next_obs);
205 assert_eq!(obs, [1.0, 2.0]);
206 assert_eq!(next_obs, [3.0, 4.0]);
207 assert_eq!(a, 0);
208 assert_eq!(r, 10.0);
209 assert!(!d);
210
211 let (a, r, d) = buf.read_into(1, &mut obs, &mut next_obs);
212 assert_eq!(obs, [5.0, 6.0]);
213 assert_eq!(next_obs, [7.0, 8.0]);
214 assert_eq!(a, 1);
215 assert_eq!(r, 20.0);
216 assert!(d);
217 }
218
219 #[test]
220 fn test_ring_wraparound_evicts_oldest() {
221 let mut buf = ReplayBuffer::new(4, 1);
225 for i in 0..6 {
226 buf.push(&[i as f32], i as i64, i as f32, &[(i + 100) as f32], false);
227 }
228 assert_eq!(buf.len(), 4, "len must saturate at capacity");
229
230 let mut found_actions = std::collections::HashSet::new();
232 let mut obs_scratch = [0.0f32; 1];
233 let mut next_scratch = [0.0f32; 1];
234 for i in 0..buf.len() {
235 let (a, _, _) = buf.read_into(i, &mut obs_scratch, &mut next_scratch);
236 found_actions.insert(a);
237 }
238
239 assert!(!found_actions.contains(&0), "oldest transition 0 not evicted");
242 assert!(!found_actions.contains(&1), "oldest transition 1 not evicted");
243 assert!(found_actions.contains(&2));
244 assert!(found_actions.contains(&3));
245 assert!(found_actions.contains(&4));
246 assert!(found_actions.contains(&5));
247 }
248
249 #[test]
250 fn test_is_ready() {
251 let mut buf = ReplayBuffer::new(8, 2);
252 assert!(!buf.is_ready(1));
253 for _ in 0..4 {
254 buf.push(&[0.0, 0.0], 0, 0.0, &[0.0, 0.0], false);
255 }
256 assert!(buf.is_ready(4));
257 assert!(!buf.is_ready(5));
258 }
259
260 #[test]
261 #[should_panic(expected = "capacity must be > 0")]
262 fn test_zero_capacity_panics() {
263 let _ = ReplayBuffer::new(0, 4);
264 }
265
266 #[test]
267 #[should_panic(expected = "obs_dim must be > 0")]
268 fn test_zero_obs_dim_panics() {
269 let _ = ReplayBuffer::new(4, 0);
270 }
271}