1#[derive(Debug, Clone)]
43pub struct ContinuousReplayBuffer {
44 obs_dim: usize,
45 action_dim: usize,
46 capacity: usize,
47 len: usize,
48 write_idx: usize,
49
50 observations: Vec<f32>,
51 actions: Vec<f32>,
52 rewards: Vec<f32>,
53 next_observations: Vec<f32>,
54 dones: Vec<bool>,
55}
56
57impl ContinuousReplayBuffer {
58 pub fn new(capacity: usize, obs_dim: usize, action_dim: usize) -> Self {
69 assert!(capacity > 0, "ContinuousReplayBuffer capacity must be > 0");
70 assert!(obs_dim > 0, "ContinuousReplayBuffer obs_dim must be > 0");
71 assert!(action_dim > 0, "ContinuousReplayBuffer action_dim must be > 0");
72
73 Self {
74 obs_dim,
75 action_dim,
76 capacity,
77 len: 0,
78 write_idx: 0,
79 observations: vec![0.0; capacity * obs_dim],
80 actions: vec![0.0; capacity * action_dim],
81 rewards: vec![0.0; capacity],
82 next_observations: vec![0.0; capacity * obs_dim],
83 dones: vec![false; capacity],
84 }
85 }
86
87 pub fn push(&mut self, obs: &[f32], action: &[f32], reward: f32, next_obs: &[f32], done: bool) {
104 debug_assert_eq!(
105 obs.len(),
106 self.obs_dim,
107 "ContinuousReplayBuffer::push: obs.len() ({}) != obs_dim ({})",
108 obs.len(),
109 self.obs_dim
110 );
111 debug_assert_eq!(
112 next_obs.len(),
113 self.obs_dim,
114 "ContinuousReplayBuffer::push: next_obs.len() ({}) != obs_dim ({})",
115 next_obs.len(),
116 self.obs_dim
117 );
118 debug_assert_eq!(
119 action.len(),
120 self.action_dim,
121 "ContinuousReplayBuffer::push: action.len() ({}) != action_dim ({})",
122 action.len(),
123 self.action_dim
124 );
125
126 let obs_start = self.write_idx * self.obs_dim;
127 let obs_end = obs_start + self.obs_dim;
128 self.observations[obs_start..obs_end].copy_from_slice(obs);
129 self.next_observations[obs_start..obs_end].copy_from_slice(next_obs);
130
131 let act_start = self.write_idx * self.action_dim;
132 let act_end = act_start + self.action_dim;
133 self.actions[act_start..act_end].copy_from_slice(action);
134
135 self.rewards[self.write_idx] = reward;
136 self.dones[self.write_idx] = done;
137
138 self.write_idx = (self.write_idx + 1) % self.capacity;
139 if self.len < self.capacity {
140 self.len += 1;
141 }
142 }
143
144 pub fn len(&self) -> usize {
146 self.len
147 }
148
149 pub fn is_empty(&self) -> bool {
151 self.len == 0
152 }
153
154 pub fn capacity(&self) -> usize {
156 self.capacity
157 }
158
159 pub fn obs_dim(&self) -> usize {
161 self.obs_dim
162 }
163
164 pub fn action_dim(&self) -> usize {
166 self.action_dim
167 }
168
169 pub fn is_ready(&self, min_size: usize) -> bool {
175 self.len >= min_size
176 }
177
178 pub(super) fn read_into(
186 &self,
187 i: usize,
188 obs_out: &mut [f32],
189 action_out: &mut [f32],
190 next_obs_out: &mut [f32],
191 ) -> (f32, bool) {
192 assert!(
193 i < self.len,
194 "ContinuousReplayBuffer::read_into: i ({}) >= len ({})",
195 i,
196 self.len
197 );
198 assert_eq!(obs_out.len(), self.obs_dim);
199 assert_eq!(next_obs_out.len(), self.obs_dim);
200 assert_eq!(action_out.len(), self.action_dim);
201
202 let obs_start = i * self.obs_dim;
203 let obs_end = obs_start + self.obs_dim;
204 obs_out.copy_from_slice(&self.observations[obs_start..obs_end]);
205 next_obs_out.copy_from_slice(&self.next_observations[obs_start..obs_end]);
206
207 let act_start = i * self.action_dim;
208 let act_end = act_start + self.action_dim;
209 action_out.copy_from_slice(&self.actions[act_start..act_end]);
210
211 (self.rewards[i], self.dones[i])
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_push_and_len() {
221 let mut buf = ContinuousReplayBuffer::new(8, 3, 2);
222 assert!(buf.is_empty());
223 assert_eq!(buf.capacity(), 8);
224 assert_eq!(buf.obs_dim(), 3);
225 assert_eq!(buf.action_dim(), 2);
226
227 buf.push(&[1.0, 2.0, 3.0], &[0.1, 0.2], 0.5, &[4.0, 5.0, 6.0], false);
228 assert_eq!(buf.len(), 1);
229 assert!(!buf.is_empty());
230
231 for i in 0..5 {
232 buf.push(
233 &[i as f32; 3],
234 &[i as f32, i as f32 + 0.5],
235 i as f32,
236 &[(i + 1) as f32; 3],
237 false,
238 );
239 }
240 assert_eq!(buf.len(), 6);
241 }
242
243 #[test]
244 fn test_roundtrip_values() {
245 let mut buf = ContinuousReplayBuffer::new(4, 2, 3);
247 buf.push(&[1.0, 2.0], &[0.1, 0.2, 0.3], 10.0, &[3.0, 4.0], false);
248 buf.push(&[5.0, 6.0], &[0.4, 0.5, 0.6], 20.0, &[7.0, 8.0], true);
249
250 let mut obs = [0.0f32; 2];
251 let mut action = [0.0f32; 3];
252 let mut next_obs = [0.0f32; 2];
253 let (r, d) = buf.read_into(0, &mut obs, &mut action, &mut next_obs);
254 assert_eq!(obs, [1.0, 2.0]);
255 assert_eq!(action, [0.1, 0.2, 0.3]);
256 assert_eq!(next_obs, [3.0, 4.0]);
257 assert_eq!(r, 10.0);
258 assert!(!d);
259
260 let (r, d) = buf.read_into(1, &mut obs, &mut action, &mut next_obs);
261 assert_eq!(obs, [5.0, 6.0]);
262 assert_eq!(action, [0.4, 0.5, 0.6]);
263 assert_eq!(next_obs, [7.0, 8.0]);
264 assert_eq!(r, 20.0);
265 assert!(d);
266 }
267
268 #[test]
269 fn test_ring_wraparound_evicts_oldest() {
270 let mut buf = ContinuousReplayBuffer::new(4, 1, 2);
273 for i in 0..6 {
274 buf.push(&[i as f32], &[i as f32, -(i as f32)], i as f32, &[(i + 100) as f32], false);
275 }
276 assert_eq!(buf.len(), 4, "len must saturate at capacity");
277
278 let mut found_rewards = std::collections::HashSet::new();
280 let mut obs_scratch = [0.0f32; 1];
281 let mut action_scratch = [0.0f32; 2];
282 let mut next_scratch = [0.0f32; 1];
283 for i in 0..buf.len() {
284 let (r, _) = buf.read_into(i, &mut obs_scratch, &mut action_scratch, &mut next_scratch);
285 assert_eq!(action_scratch, [r, -r]);
287 found_rewards.insert(r as i64);
288 }
289
290 assert!(!found_rewards.contains(&0), "oldest transition 0 not evicted");
293 assert!(!found_rewards.contains(&1), "oldest transition 1 not evicted");
294 assert!(found_rewards.contains(&2));
295 assert!(found_rewards.contains(&3));
296 assert!(found_rewards.contains(&4));
297 assert!(found_rewards.contains(&5));
298 }
299
300 #[test]
301 fn test_is_ready() {
302 let mut buf = ContinuousReplayBuffer::new(8, 2, 1);
303 assert!(!buf.is_ready(1));
304 for _ in 0..4 {
305 buf.push(&[0.0, 0.0], &[0.0], 0.0, &[0.0, 0.0], false);
306 }
307 assert!(buf.is_ready(4));
308 assert!(!buf.is_ready(5));
309 }
310
311 #[test]
312 #[should_panic(expected = "capacity must be > 0")]
313 fn test_zero_capacity_panics() {
314 let _ = ContinuousReplayBuffer::new(0, 4, 2);
315 }
316
317 #[test]
318 #[should_panic(expected = "obs_dim must be > 0")]
319 fn test_zero_obs_dim_panics() {
320 let _ = ContinuousReplayBuffer::new(4, 0, 2);
321 }
322
323 #[test]
324 #[should_panic(expected = "action_dim must be > 0")]
325 fn test_zero_action_dim_panics() {
326 let _ = ContinuousReplayBuffer::new(4, 2, 0);
327 }
328}