1use burn::tensor::{Tensor as BurnTensor, TensorData, backend::Backend};
16use rand::Rng;
17
18use super::continuous_storage::ContinuousReplayBuffer;
19
20#[derive(Debug, Clone)]
26pub struct ContinuousReplayBatch {
27 pub observations: Vec<f32>,
29 pub actions: Vec<f32>,
31 pub rewards: Vec<f32>,
33 pub next_observations: Vec<f32>,
35 pub dones: Vec<bool>,
38 pub obs_dim: usize,
40 pub action_dim: usize,
42}
43
44impl ContinuousReplayBatch {
45 pub fn len(&self) -> usize {
47 self.rewards.len()
48 }
49
50 pub fn is_empty(&self) -> bool {
52 self.rewards.is_empty()
53 }
54
55 pub fn to_burn_tensors<B: Backend>(
68 &self,
69 device: &B::Device,
70 ) -> ContinuousReplayBurnTensors<B> {
71 let batch = self.len();
72 let obs_dim = self.obs_dim;
73 let action_dim = self.action_dim;
74
75 let observations = BurnTensor::<B, 2>::from_data(
79 TensorData::new(self.observations.clone(), [batch, obs_dim]),
80 device,
81 );
82 let next_observations = BurnTensor::<B, 2>::from_data(
83 TensorData::new(self.next_observations.clone(), [batch, obs_dim]),
84 device,
85 );
86 let actions = BurnTensor::<B, 2>::from_data(
87 TensorData::new(self.actions.clone(), [batch, action_dim]),
88 device,
89 );
90 let rewards =
91 BurnTensor::<B, 1>::from_data(TensorData::new(self.rewards.clone(), [batch]), device);
92 let dones_f: Vec<f32> = self.dones.iter().map(|&d| if d { 1.0 } else { 0.0 }).collect();
93 let dones = BurnTensor::<B, 1>::from_data(TensorData::new(dones_f, [batch]), device);
94
95 ContinuousReplayBurnTensors { observations, actions, rewards, next_observations, dones }
96 }
97}
98
99#[derive(Debug)]
107pub struct ContinuousReplayBurnTensors<B: Backend> {
108 pub observations: BurnTensor<B, 2>,
110 pub actions: BurnTensor<B, 2>,
112 pub rewards: BurnTensor<B, 1>,
114 pub next_observations: BurnTensor<B, 2>,
116 pub dones: BurnTensor<B, 1>,
119}
120
121pub fn sample<R: Rng>(
131 buffer: &ContinuousReplayBuffer,
132 batch_size: usize,
133 rng: &mut R,
134) -> ContinuousReplayBatch {
135 assert!(!buffer.is_empty(), "ContinuousReplayBuffer is empty; cannot sample");
136 assert!(batch_size > 0, "batch_size must be > 0");
137
138 let obs_dim = buffer.obs_dim();
139 let action_dim = buffer.action_dim();
140 let len = buffer.len();
141
142 let mut observations = vec![0.0f32; batch_size * obs_dim];
143 let mut next_observations = vec![0.0f32; batch_size * obs_dim];
144 let mut actions = vec![0.0f32; batch_size * action_dim];
145 let mut rewards = Vec::with_capacity(batch_size);
146 let mut dones = Vec::with_capacity(batch_size);
147
148 for k in 0..batch_size {
149 let idx = rng.random_range(0..len);
150 let obs_slice = &mut observations[k * obs_dim..(k + 1) * obs_dim];
151 let next_slice = &mut next_observations[k * obs_dim..(k + 1) * obs_dim];
152 let action_slice = &mut actions[k * action_dim..(k + 1) * action_dim];
153 let (r, d) = buffer.read_into(idx, obs_slice, action_slice, next_slice);
154 rewards.push(r);
155 dones.push(d);
156 }
157
158 ContinuousReplayBatch {
159 observations,
160 actions,
161 rewards,
162 next_observations,
163 dones,
164 obs_dim,
165 action_dim,
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use rand::{SeedableRng, rngs::StdRng};
172
173 use super::*;
174
175 #[test]
176 fn test_sample_returns_correct_count() {
177 let mut buf = ContinuousReplayBuffer::new(16, 3, 2);
178 for i in 0..10 {
179 buf.push(
180 &[i as f32, i as f32 + 0.1, i as f32 + 0.2],
181 &[i as f32, -(i as f32)],
182 i as f32,
183 &[(i + 1) as f32, (i + 1) as f32 + 0.1, (i + 1) as f32 + 0.2],
184 false,
185 );
186 }
187 let mut rng = StdRng::seed_from_u64(42);
188 let batch = sample(&buf, 5, &mut rng);
189 assert_eq!(batch.len(), 5);
190 assert_eq!(batch.rewards.len(), 5);
191 assert_eq!(batch.dones.len(), 5);
192 assert_eq!(batch.observations.len(), 5 * 3);
193 assert_eq!(batch.next_observations.len(), 5 * 3);
194 assert_eq!(batch.actions.len(), 5 * 2);
195 assert_eq!(batch.obs_dim, 3);
196 assert_eq!(batch.action_dim, 2);
197 }
198
199 #[test]
200 fn test_sampled_values_match_pushed_values() {
201 let mut buf = ContinuousReplayBuffer::new(8, 2, 3);
203 buf.push(&[7.0, 8.0], &[0.1, 0.2, 0.3], 42.0, &[9.0, 10.0], true);
204
205 let mut rng = StdRng::seed_from_u64(0);
206 let batch = sample(&buf, 4, &mut rng);
207 for k in 0..4 {
208 assert_eq!(batch.rewards[k], 42.0);
209 assert!(batch.dones[k]);
210 assert_eq!(&batch.observations[k * 2..(k + 1) * 2], &[7.0, 8.0]);
211 assert_eq!(&batch.next_observations[k * 2..(k + 1) * 2], &[9.0, 10.0]);
212 assert_eq!(&batch.actions[k * 3..(k + 1) * 3], &[0.1, 0.2, 0.3]);
213 }
214 }
215
216 mod burn_tests {
217 use burn::backend::NdArray;
218
219 use super::*;
220
221 type B = NdArray<f32>;
222
223 #[test]
224 fn test_to_burn_tensors_shapes_and_roundtrip() {
225 let mut buf = ContinuousReplayBuffer::new(8, 4, 2);
226 for i in 0..6 {
227 buf.push(
228 &[i as f32; 4],
229 &[i as f32, i as f32 + 0.5],
230 i as f32,
231 &[i as f32 + 1.0; 4],
232 i == 5,
233 );
234 }
235 let mut rng = StdRng::seed_from_u64(1);
236 let batch = sample(&buf, 3, &mut rng);
237 let device = crate::utils::cuda::default_burn_device::<B>();
238 let t = batch.to_burn_tensors::<B>(&device);
239
240 assert_eq!(t.observations.dims(), [3, 4]);
242 assert_eq!(t.next_observations.dims(), [3, 4]);
243 assert_eq!(t.actions.dims(), [3, 2]);
244 assert_eq!(t.rewards.dims(), [3]);
245 assert_eq!(t.dones.dims(), [3]);
246
247 let obs_flat: Vec<f32> = t.observations.into_data().to_vec().unwrap();
250 assert_eq!(obs_flat, batch.observations);
251 let next_flat: Vec<f32> = t.next_observations.into_data().to_vec().unwrap();
252 assert_eq!(next_flat, batch.next_observations);
253 let acts: Vec<f32> = t.actions.into_data().to_vec().unwrap();
254 assert_eq!(acts, batch.actions);
255 let rews: Vec<f32> = t.rewards.into_data().to_vec().unwrap();
256 assert_eq!(rews, batch.rewards);
257 let dones_f: Vec<f32> = t.dones.into_data().to_vec().unwrap();
258 let expected_dones: Vec<f32> =
259 batch.dones.iter().map(|&d| if d { 1.0 } else { 0.0 }).collect();
260 assert_eq!(dones_f, expected_dones);
261 }
262
263 #[test]
264 fn test_to_burn_tensors_empty_batch_does_not_panic() {
265 let batch = ContinuousReplayBatch {
269 observations: vec![],
270 actions: vec![],
271 rewards: vec![],
272 next_observations: vec![],
273 dones: vec![],
274 obs_dim: 4,
275 action_dim: 2,
276 };
277 let device = crate::utils::cuda::default_burn_device::<B>();
278 let t = batch.to_burn_tensors::<B>(&device);
279 assert_eq!(t.observations.dims(), [0, 4]);
280 assert_eq!(t.next_observations.dims(), [0, 4]);
281 assert_eq!(t.actions.dims(), [0, 2]);
282 assert_eq!(t.rewards.dims(), [0]);
283 assert_eq!(t.dones.dims(), [0]);
284 }
285 }
286
287 #[test]
288 #[should_panic(expected = "ContinuousReplayBuffer is empty")]
289 fn test_sample_empty_panics() {
290 let buf = ContinuousReplayBuffer::new(4, 2, 1);
291 let mut rng = StdRng::seed_from_u64(0);
292 let _ = sample(&buf, 2, &mut rng);
293 }
294
295 #[test]
296 #[should_panic(expected = "batch_size must be > 0")]
297 fn test_zero_batch_size_panics() {
298 let mut buf = ContinuousReplayBuffer::new(4, 2, 1);
299 buf.push(&[0.0, 0.0], &[0.0], 0.0, &[0.0, 0.0], false);
300 let mut rng = StdRng::seed_from_u64(0);
301 let _ = sample(&buf, 0, &mut rng);
302 }
303}